# RosterElf Website - AI Context
This file contains a structured representation of the RosterElf website content,
designed for easy consumption by AI systems, chatbots, and automated tools.
## About This File
**Purpose**: Provide AI agents with comprehensive, structured access to RosterElf's
website content including features, guides, blog posts, and documentation.
**Format**: Markdown with file paths and line numbers
- Each section begins with: ## File: path/to/file
- Line numbers are prefixed to each line for reference
- Content is organized by topic and importance
**Usage**: This file is read-only. Use file paths to navigate between sections.
Changes should be made to the original website files, not this packed version.
**Last Updated**: 2026-02-26
---
# Files
## File: src/components/comparison/ComparisonHelpers.astro
````astro
1: ---
2: /**
3: * Reusable components for comparison pages
4: * Import these in your comparison page content
5: */
6: import { Check, X } from 'lucide-astro'
7:
8: // Export props interfaces for use in pages
9: export interface ComparisonTableRow {
10: feature: string
11: platform1: string | boolean | number
12: platform2: string | boolean | number
13: }
14:
15: interface Props {
16: // You can add shared props here if needed
17: }
18: ---
19:
20:
````
## File: src/components/AgedCareAwardCalculator.astro
````astro
1: ---
2: // Aged Care Award Rate Calculator Component
3: // Data from 2025/26 Aged Care Award rates (MA000018)
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: // General classification (Levels 1-7) - effective 1 July 2025
10: general_permanent: {
11: 'Level 1': 26.51,
12: 'Level 2': 27.56,
13: 'Level 3': 28.62,
14: 'Level 4': 28.96,
15: 'Level 5': 29.94,
16: 'Level 6': 31.55,
17: 'Level 7': 32.12,
18: },
19: general_casual: {
20: 'Level 1': 33.14, // 26.51 × 1.25
21: 'Level 2': 34.45, // 27.56 × 1.25
22: 'Level 3': 35.78, // 28.62 × 1.25
23: 'Level 4': 36.2, // 28.96 × 1.25
24: 'Level 5': 37.43, // 29.94 × 1.25
25: 'Level 6': 39.44, // 31.55 × 1.25
26: 'Level 7': 40.15, // 32.12 × 1.25
27: },
28: // Direct care classification (Levels 1-6) - effective 1 October 2025
29: directcare_permanent: {
30: 'Level 1': 31.13,
31: 'Level 2': 32.86,
32: 'Level 3': 34.59,
33: 'Level 4': 35.97,
34: 'Level 5': 37.35,
35: 'Level 6': 38.74,
36: },
37: directcare_casual: {
38: 'Level 1': 38.91, // 31.13 × 1.25
39: 'Level 2': 41.08, // 32.86 × 1.25
40: 'Level 3': 43.24, // 34.59 × 1.25
41: 'Level 4': 44.96, // 35.97 × 1.25
42: 'Level 5': 46.69, // 37.35 × 1.25
43: 'Level 6': 48.43, // 38.74 × 1.25
44: },
45: }
46:
47: // Fixed award rules - non-editable, shows how the award works
48: const awardRules = [
49: {
50: id: 'ordinary',
51: name: 'Ordinary hours (Mon-Fri)',
52: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
53: startTime: '06:00',
54: endTime: '18:00',
55: penaltyMultiplier: { permanent: 1.0, casual: 1.25 }, // Casual gets 25% loading
56: },
57: {
58: id: 'evening',
59: name: 'Evening shift allowance',
60: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
61: startTime: '18:00',
62: endTime: '00:00',
63: penaltyMultiplier: { permanent: 1.15, casual: 1.4375 }, // 15% allowance + 25% casual loading (1.15 × 1.25)
64: },
65: {
66: id: 'night',
67: name: 'Night shift allowance',
68: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
69: startTime: '00:00',
70: endTime: '06:00',
71: penaltyMultiplier: { permanent: 1.15, casual: 1.4375 }, // 15% allowance + 25% casual loading (1.15 × 1.25)
72: },
73: {
74: id: 'saturday',
75: name: 'Saturday (all day)',
76: days: ['SAT'],
77: startTime: '00:00',
78: endTime: '00:00',
79: penaltyMultiplier: { permanent: 1.5, casual: 1.75 }, // Weekend rates substitute for casual loading
80: },
81: {
82: id: 'sunday',
83: name: 'Sunday (all day)',
84: days: ['SUN'],
85: startTime: '00:00',
86: endTime: '00:00',
87: penaltyMultiplier: { permanent: 1.75, casual: 2.0 }, // Weekend rates substitute for casual loading
88: },
89: {
90: id: 'publicholiday',
91: name: 'Public holiday',
92: days: ['HOL'],
93: startTime: '00:00',
94: endTime: '00:00',
95: penaltyMultiplier: { permanent: 2.5, casual: 2.75 }, // Public holiday rate substitutes for casual loading
96: },
97: ]
98: ---
99:
100:
101:
102:
AWARD RATE ESTIMATOR
103:
See how RosterElf interprets the Aged Care Award
104:
105: This is an educational example showing how the Aged Care Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
106: employment type, and shift times.
107:
108:
109:
110:
111:
112:
113:
114:
115:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
116:
official Fair Work pay guide
119: and consulting your Award obligations.
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
Classification type
130:
131:
132:
133:
134:
138: General
139: Direct Care
140:
141:
142:
143:
144:
145:
146:
Employment type
147:
148:
149:
150:
151:
155: Full-time / Part-time
156: Casual (includes 25% loading)
157:
158:
159:
160:
161:
162:
163:
Classification level
164:
165:
166:
167:
168:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
Base ordinary rate
184:
Mon-Fri, standard hours
185:
186:
187:
188: $
189: 26.51
190: /hr
191:
192:
193:
194:
195:
196:
197:
198:
199:
200: Aged Care Award penalty rates
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212: Example weekly cost (38 hours)
213:
214:
215:
216:
217:
218:
219:
220: Example total:
221: $1,007.38
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
Example only - not for payroll use
232:
This is a demonstration of how RosterElf calculates award-compliant rates.
233:
234:
235:
242: Important: Read all before using this calculator
243:
244:
245:
246:
247:
248:
249:
250:
The actual cost for your employees will depend on:
251:
252:
253: Their specific classification level and employment type
254: Actual hours worked and shift times
255: Any additional allowances, overtime, or enterprise agreement provisions
256: Current award rates (which change annually in July)
257:
258:
259:
260:
Calculator Limitation
261:
262: This calculator displays simplified time-based penalties (e.g., "evening 6pm–midnight") for demonstration purposes. The actual Aged Care Award has more complex shift allowance rules
263: based on when shifts start , not just the hours worked. Always verify penalty rates with the official Fair Work pay guide or PACT tool for your specific shift patterns.
264:
265:
266:
267:
For accurate payroll calculations, always:
268:
269:
270:
271: Verify current rates with the official Fair Work pay guide
277:
278: Confirm your employees' correct award coverage and classification
279: Use award interpretation software or consult a payroll professional
280: Review your specific enterprise agreement (if applicable)
281:
282:
283:
Do not rely on this example for actual wage payments.
284:
285:
286:
287:
288:
289:
290:
291:
292:
293:
Stop calculating penalty rates manually
294:
Let RosterElf handle award compliance automatically
295:
296:
297: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
298: the work for you.
299:
300:
301:
302:
303:
304:
305:
306:
307:
308: No credit card required
309:
310:
311:
312: Full access
313:
314:
315:
316: 24/7 support
317:
318:
319:
320:
321:
322:
323:
How RosterElf automates award calculations
324:
325:
326:
327:
328:
332:
Create pay templates
333:
334: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
335: each shift based on the employee's classification, shift timing, and employment type.
336:
337:
Award interpretation →
338:
339:
340:
341:
342:
346:
Define rate rules
347:
348: Configure when different penalty rates apply (evenings, weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
349:
350:
Penalty rates guide →
351:
352:
353:
354:
355:
359:
Auto-apply to shifts
360:
361: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
362:
363:
Payroll integration →
364:
365:
366:
367:
368:
369:
Learn more:
370:
377:
378:
379:
380:
381:
382:
651:
652:
````
## File: src/components/AwardRatesCallout.astro
````astro
1: ---
2: import { ArrowRight, BookOpen } from 'lucide-astro'
3: import { getAwardRate } from '../data/industryMappings'
4:
5: interface Props {
6: industry: string
7: background?: 'white' | 'primary'
8: }
9:
10: const { industry, background = 'primary' } = Astro.props
11:
12: const awardRate = getAwardRate(industry)
13:
14: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
15: const cardBgClass = background === 'white' ? 'bg-primary-50' : 'bg-white'
16: ---
17:
18: {
19: awardRate && (
20:
46: )
47: }
````
## File: src/components/BlogResourceCards.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import { readFile } from 'node:fs/promises'
4: import { join } from 'node:path'
5:
6: interface BlogPost {
7: title: string
8: excerpt: string
9: href: string
10: image: string
11: category: string
12: readTime: string
13: }
14:
15: interface Props {
16: chip?: string
17: title?: string
18: description?: string
19: posts?: BlogPost[]
20: background?: 'primary' | 'white'
21: }
22:
23: const defaultPosts: BlogPost[] = [
24: {
25: title: 'Rostering strategy planning for the new year',
26: excerpt: 'Plan your rostering strategy for the new year. Review performance, set goals, forecast capacity, and implement improvements.',
27: href: '/blog/rostering-strategy-planning',
28: image: '/images/stock/unsplash-1600880292089-90a7e086ee0c.webp',
29: category: 'Rostering & Scheduling',
30: readTime: '9 min read',
31: },
32: {
33: title: 'Fair and cost-effective rostering for Australian SMEs',
34: excerpt: 'Learn how Australian businesses build fair, cost-effective rosters while staying award compliant.',
35: href: '/blog/fair-cost-effective-employee-rostering-australia',
36: image: '/images/stock/unsplash-1553484771-898ed465e931.webp',
37: category: 'Rostering & Scheduling',
38: readTime: '9 min read',
39: },
40: {
41: title: 'Reviewing rostering KPIs from the past year',
42: excerpt: 'Review rostering KPIs to improve next year. Analyse labour costs, roster accuracy, overtime rates, and set realistic targets.',
43: href: '/blog/rostering-kpis-review',
44: image: '/images/stock/unsplash-1573165231977-3f0e27806045.webp',
45: category: 'Rostering & Scheduling',
46: readTime: '9 min read',
47: },
48: ]
49:
50: /**
51: * Check if a blog post has a future premiere date
52: */
53: async function hasFuturePremiereDate(blogSlug: string): Promise {
54: try {
55: const filePath = join(process.cwd(), 'src/pages/blog', `${blogSlug}.astro`)
56: const content = await readFile(filePath, 'utf-8')
57:
58: // Extract premiere date from content
59: const premiereDateMatch = content.match(/premiereDate\s*=\s*['"]([^'"]+)['"]/)
60: if (!premiereDateMatch) return false
61:
62: const premiereDate = new Date(premiereDateMatch[1])
63: const now = new Date()
64:
65: // Compare dates at midnight
66: now.setHours(0, 0, 0, 0)
67: premiereDate.setHours(0, 0, 0, 0)
68:
69: return premiereDate > now
70: } catch {
71: // If file doesn't exist or can't be read, assume it's not a blog post or is published
72: return false
73: }
74: }
75:
76: /**
77: * Filter out blog posts with future premiere dates
78: */
79: async function filterPublishedPosts(posts: BlogPost[]): Promise {
80: const results = await Promise.all(
81: posts.map(async (post) => {
82: // Only check blog posts (href starts with /blog/)
83: if (!post.href.startsWith('/blog/')) return { post, isPublished: true }
84:
85: // Extract slug from href
86: const slug = post.href.replace('/blog/', '')
87: const isFuture = await hasFuturePremiereDate(slug)
88:
89: return { post, isPublished: !isFuture }
90: })
91: )
92:
93: return results.filter((r) => r.isPublished).map((r) => r.post)
94: }
95:
96: const {
97: chip = 'RESOURCES',
98: title = 'Learn more about rostering',
99: description = 'Guides and articles to help you master staff scheduling and compliance.',
100: posts = defaultPosts,
101: background = 'white',
102: } = Astro.props
103:
104: // Filter out unpublished posts
105: const publishedPosts = await filterPublishedPosts(posts)
106:
107: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
108: ---
109:
110:
111:
112:
113:
{chip}
114:
{title}
115:
{description}
116:
117:
118:
140:
141:
View all articles →
142:
143:
````
## File: src/components/BookCallModal.astro
````astro
1: ---
2: // Book a Call Modal - lets users choose between demo (sales) or support calls
3: ---
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
Book a call with us
18:
Choose the option that best fits your needs
19:
20:
21:
22:
86:
87:
88:
89:
````
## File: src/components/BookmarkModal.astro
````astro
1: ---
2: /**
3: * BookmarkModal Component
4: *
5: * Reusable bookmark prompt modal for free tools pages.
6: * Shows keyboard shortcuts to bookmark the page.
7: *
8: * Usage:
9: * 1. Import and add to your page
10: * 2. Add class="bookmark-btn" to any button that should trigger the modal
11: *
12: * Props:
13: * - showCard: Show a card section with bookmark button (like free-roster-tool)
14: * - message: Custom message for the modal
15: * - cardTitle: Title for the card section
16: * - cardSubtitle: Subtitle for the card section
17: */
18:
19: import { Bookmark } from 'lucide-astro'
20:
21: interface Props {
22: /** Custom message explaining why to bookmark */
23: message?: string
24: /** Show a card section with bookmark button */
25: showCard?: boolean
26: /** Card title */
27: cardTitle?: string
28: /** Card subtitle */
29: cardSubtitle?: string
30: }
31:
32: const {
33: message = 'Bookmark this page to return anytime — your data is saved in your browser.',
34: showCard = false,
35: cardTitle = 'Bookmark this page',
36: cardSubtitle = 'Save this page to your bookmarks for quick access anytime.',
37: } = Astro.props
38: ---
39:
40: {
41: showCard && (
42:
43:
44:
45:
46:
47:
48:
49:
{cardTitle}
50:
{cardSubtitle}
51:
52:
53:
54: Bookmark
55:
56:
57:
58:
59: )
60: }
61:
62:
63:
64:
65:
66:
Bookmark this page
67:
68:
69:
70:
71:
72:
73:
74:
77:
Press the following keys to bookmark:
78:
79:
80: Mac
81: ⌘
82: +
83: D
84:
85:
86: Windows
87: Ctrl
88: +
89: D
90:
91:
92:
93:
Got it
94:
95:
96:
97:
````
## File: src/components/Breadcrumb.astro
````astro
1: ---
2: import { ChevronRight } from 'lucide-astro'
3:
4: interface BreadcrumbItem {
5: label: string
6: href: string
7: }
8:
9: interface Props {
10: items: BreadcrumbItem[]
11: variant?: 'default' | 'highlighted' | 'highlighted-white'
12: }
13:
14: const { items, variant = 'default' } = Astro.props
15:
16: // Prefix hrefs with /uk when rendered on a UK page
17: const isUK = Astro.url.pathname.startsWith('/uk')
18: const prefixHref = (href: string) => {
19: if (!isUK || !href.startsWith('/') || href.startsWith('/uk')) return href
20: return href === '/' ? '/uk' : '/uk' + href
21: }
22: const prefixedItems = items.map((item) => ({ ...item, href: prefixHref(item.href) }))
23:
24: // Separate the last item (current page) from the rest
25: const navigationItems = prefixedItems.slice(0, -1)
26: const currentItem = prefixedItems[prefixedItems.length - 1]
27:
28: const bgClass = variant === 'highlighted' ? 'bg-primary-100' : variant === 'highlighted-white' ? 'bg-white' : ''
29:
30: // Generate BreadcrumbList JSON-LD schema
31: const breadcrumbSchema = {
32: '@context': 'https://schema.org',
33: '@type': 'BreadcrumbList',
34: itemListElement: prefixedItems.map((item, index) => ({
35: '@type': 'ListItem',
36: position: index + 1,
37: name: item.label,
38: item: item.href.startsWith('http') ? item.href : `https://www.rosterelf.com${item.href}`,
39: })),
40: }
41: ---
42:
43:
44:
45: {
46: variant === 'highlighted' || variant === 'highlighted-white' ? (
47:
48:
49: {navigationItems.map((item) => (
50: <>
51:
52: {item.label}
53:
54:
55: >
56: ))}
57:
{currentItem.label}
58:
59:
60: ) : (
61:
62: {navigationItems.map((item) => (
63: <>
64:
65: {item.label}
66:
67:
68: >
69: ))}
70: {currentItem.label}
71:
72: )
73: }
````
## File: src/components/CardGrid.astro
````astro
1: ---
2: import { ArrowRight, Calendar, Clock, Users, Smartphone, Link2, FileCheck } from 'lucide-astro'
3:
4: interface CardItem {
5: name: string
6: logo?: string
7: logoAlt?: string
8: logoScale?: number
9: vsImage?: string
10: vsImageAlt?: string
11: icon?: string
12: heroImage?: string
13: heroImageAlt?: string
14: title: string
15: description: string
16: href?: string
17: isActive?: boolean
18: score?: string
19: lastUpdated?: string
20: }
21:
22: interface Props {
23: chip?: string
24: title: string
25: description?: string
26: items: CardItem[]
27: background?: 'white' | 'primary'
28: linkText?: string
29: viewAllLink?: string
30: viewAllText?: string
31: }
32:
33: const { chip, title, description, items, background = 'white', linkText = 'Read more', viewAllLink, viewAllText } = Astro.props
34:
35: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
36:
37: // Icon mapping for buying guides
38: const iconMap: Record = {
39: Calendar,
40: Clock,
41: Users,
42: Smartphone,
43: Link2,
44: FileCheck,
45: }
46: ---
47:
48:
49:
50:
51: {chip &&
{chip} }
52:
{title}
53: {description &&
{description}
}
54:
55:
56:
142:
143: {
144: viewAllLink && viewAllText && (
145:
146: {viewAllText}
147:
148:
149: )
150: }
151:
152:
````
## File: src/components/CaseStudyCallout.astro
````astro
1: ---
2: import { ArrowRight, Users } from 'lucide-astro'
3: import { getIndustryInfo } from '../data/industryMappings'
4:
5: interface Props {
6: industry: string
7: background?: 'white' | 'primary'
8: }
9:
10: const { industry, background = 'white' } = Astro.props
11:
12: const industryInfo = getIndustryInfo(industry)
13: const caseStudy = industryInfo?.caseStudy
14:
15: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
16: const cardBgClass = background === 'white' ? 'bg-primary-50' : 'bg-white'
17: ---
18:
19: {
20: caseStudy && (
21:
47: )
48: }
````
## File: src/components/CollectionPageSchema.astro
````astro
1: ---
2: /**
3: * CollectionPageSchema - Generates schema.org CollectionPage + ItemList markup for hub pages
4: *
5: * This component adds structured data to help search engines understand
6: * that this page is a collection/hub of related items.
7: */
8:
9: interface ListItem {
10: name: string
11: url: string
12: description?: string
13: }
14:
15: interface Props {
16: name: string // Collection name e.g., "RosterElf Features"
17: description: string // Page meta description
18: url: string // Canonical URL path e.g., "/features"
19: items: ListItem[] // List of items in the collection
20: }
21:
22: const { name, description, url, items } = Astro.props
23:
24: const collectionSchema = {
25: '@context': 'https://schema.org',
26: '@type': 'CollectionPage',
27: name: name,
28: description: description,
29: url: `https://www.rosterelf.com${url}`,
30: provider: {
31: '@type': 'Organization',
32: name: 'RosterElf',
33: url: 'https://www.rosterelf.com',
34: },
35: mainEntity: {
36: '@type': 'ItemList',
37: numberOfItems: items.length,
38: itemListElement: items.map((item, index) => ({
39: '@type': 'ListItem',
40: position: index + 1,
41: name: item.name,
42: url: `https://www.rosterelf.com${item.url}`,
43: ...(item.description && { description: item.description }),
44: })),
45: },
46: }
47: ---
48:
49:
````
## File: src/components/CommonRolesSection.astro
````astro
1: ---
2: import { ArrowRight, FileText } from 'lucide-astro'
3: import { getIndustryRoles, hasIndustryRoles, type RoleInfo } from '../data/roleMappings'
4:
5: interface Props {
6: industrySlug: string
7: industryName: string
8: background?: 'white' | 'primary'
9: }
10:
11: const { industrySlug, industryName, background = 'white' } = Astro.props
12:
13: const roles = getIndustryRoles(industrySlug)
14: const hasRoles = roles.length > 0
15:
16: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
17: ---
18:
19: {
20: hasRoles && (
21:
22:
23:
24:
JOB DESCRIPTIONS
25:
Common roles in {industryName.toLowerCase()}
26:
Download free job description templates for your {industryName.toLowerCase()} team
27:
28:
29:
45:
46:
47: View all job descriptions
48:
49:
50:
51:
52: )
53: }
````
## File: src/components/ComparisonCarousel.astro
````astro
1: ---
2: import { Crown, Star } from 'lucide-astro'
3: import { ratingsData } from '../data/ratingsData'
4: import { competitorRatings } from '../data/competitorRatings'
5:
6: interface Props {
7: background?: 'primary' | 'white'
8: chip?: string
9: title?: string
10: description?: string
11: }
12:
13: const {
14: background = 'primary',
15: chip = "OUR REVIEWS DON'T LIE",
16: title = 'Still comparing the usual suspects?',
17: description = "Don't just take our word for it— Browse verified Xero reviews and start your free 15-day trial today.",
18: } = Astro.props
19: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
20:
21: // Get live RosterElf rating from centralized data
22: const xeroRating = ratingsData.platforms.xero
23:
24: interface Competitor {
25: name: string
26: logo: string
27: logoClass?: string
28: description: string
29: rating: number
30: reviewCount: number
31: ctaText: string
32: ctaUrl: string
33: isExternal?: boolean
34: }
35:
36: // Build competitors array from centralized data, ordered by Xero rating (highest first)
37: const competitors: Competitor[] = [
38: {
39: name: 'RosterElf',
40: logo: competitorRatings.rosterelf.logo,
41: logoClass: '-translate-y-1 w-[55%]',
42: description: 'All-in-one rostering, HR & time tracking. Top-rated Xero app with award compliance.',
43: rating: xeroRating.rating,
44: reviewCount: xeroRating.reviewCount,
45: ctaText: 'Read Reviews',
46: ctaUrl: xeroRating.url,
47: isExternal: true,
48: },
49: {
50: name: 'Connecteam',
51: logo: '/images/logos/connecteam-wordmark.png',
52: logoClass: '-translate-y-1 w-[80%]',
53: description: 'Employee scheduling, time tracking, and team communication app with Xero integration.',
54: rating: competitorRatings.connecteam.xero.rating,
55: reviewCount: competitorRatings.connecteam.xero.reviewCount,
56: ctaText: 'Read Reviews',
57: ctaUrl: competitorRatings.connecteam.xero.url!,
58: isExternal: true,
59: },
60: {
61: name: 'Deputy',
62: logo: '/images/logos/deputy.svg',
63: logoClass: '-translate-y-1 w-[60%]',
64: description: 'Staff scheduling and time tracking with Xero integration. One-click timesheet to invoice.',
65: rating: competitorRatings.deputy.xero.rating,
66: reviewCount: competitorRatings.deputy.xero.reviewCount,
67: ctaText: 'Read Reviews',
68: ctaUrl: competitorRatings.deputy.xero.url!,
69: isExternal: true,
70: },
71: {
72: name: 'Employment Hero',
73: logo: competitorRatings.employmentHero.logo,
74: logoClass: '-translate-y-1 h-12 w-auto object-contain',
75: description: 'HR, payroll & benefits platform. Syncs employee data with Xero.',
76: rating: competitorRatings.employmentHero.xero.rating,
77: reviewCount: competitorRatings.employmentHero.xero.reviewCount,
78: ctaText: 'Read Reviews',
79: ctaUrl: competitorRatings.employmentHero.xero.url!,
80: isExternal: true,
81: },
82: ]
83:
84: const getStarFill = (rating: number, starIndex: number) => {
85: const fullStars = Math.floor(rating)
86: const partialPercent = Math.round((rating % 1) * 100)
87:
88: if (starIndex < fullStars) return 100
89: if (starIndex === fullStars) return partialPercent
90: return 0
91: }
92: ---
93:
94:
95:
96:
97:
{chip}
98:
{title}
99:
{description}
100:
101:
102:
103:
157:
158:
159:
212:
213:
````
## File: src/components/ComparisonGrid.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3:
4: interface ComparisonItem {
5: name: string
6: logo?: string
7: logoAlt?: string
8: logoText?: string
9: title: string
10: description: string
11: href?: string
12: isActive?: boolean
13: }
14:
15: interface Props {
16: chip?: string
17: title?: string
18: description?: string
19: items: ComparisonItem[]
20: background?: 'white' | 'primary'
21: }
22:
23: const {
24: chip = 'HEAD-TO-HEAD',
25: title = 'Detailed software comparisons',
26: description = 'In-depth, side-by-side comparisons covering pricing, features, compliance, support, and real user reviews.',
27: items,
28: background = 'white',
29: } = Astro.props
30:
31: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
32: ---
33:
34:
35:
36:
37:
{chip}
38:
{title}
39:
{description}
40:
41:
42:
82:
83:
````
## File: src/components/CTASection.astro
````astro
1: ---
2: import { Star } from 'lucide-astro'
3: import TrialButton from './TrialButton.astro'
4:
5: interface Props {
6: title?: string
7: description?: string
8: landing?: string
9: background?: 'primary' | 'white'
10: }
11:
12: const {
13: title = 'Start your free 15 day RosterElf trial today',
14: description = 'Join 30,000+ Australian businesses using RosterElf to simplify rostering, payroll and HR.',
15: background = 'primary',
16: } = Astro.props
17:
18: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
19: ---
20:
21:
22:
23:
{title}
24:
{description}
25:
36:
37:
38:
39: {Array.from({ length: 5 }).map(() => )}
40:
41:
4.8 stars by 1,570 users
42:
43:
•
44:
100+ countries
45:
•
46:
30,000+ users
47:
48:
49:
````
## File: src/components/DisclaimerSection.astro
````astro
1: ---
2: /**
3: * DisclaimerSection - Legal disclaimer component
4: *
5: * Props:
6: * - context: string - The specific context for this disclaimer
7: * - title: string (optional) - Custom title override
8: * - variant: 'legal' | 'tax' | 'general' - Sets default title
9: * - collapsible: boolean (optional) - Makes content collapsible on mobile (default: false)
10: * - defaultExpanded: boolean (optional) - If collapsible, start expanded (default: false on mobile, true on desktop)
11: *
12: * When collapsible=true:
13: * - Header "Important disclaimer" is always visible (legal requirement)
14: * - Content is collapsed by default on mobile, expanded on desktop
15: * - Click/tap header to expand/collapse
16: * - Uses CSS + minimal JS for accessibility (aria-expanded)
17: */
18: import { Scale, ExternalLink, ChevronDown } from 'lucide-astro'
19:
20: interface Props {
21: context: string
22: title?: string
23: variant?: 'legal' | 'tax' | 'general'
24: collapsible?: boolean
25: defaultExpanded?: boolean
26: background?: 'white' | 'primary'
27: fullWidth?: boolean
28: maxWidth?: string
29: }
30:
31: const { context, title, variant = 'legal', collapsible = false, defaultExpanded = false, background = 'white', fullWidth = false, maxWidth = 'max-w-4xl' } = Astro.props
32:
33: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
34: const cardBgClass = background === 'primary' ? 'bg-white' : 'bg-primary-50'
35:
36: const titles = {
37: legal: 'General information only – not legal advice',
38: tax: 'General information only – not tax or legal advice',
39: general: 'General information only',
40: }
41:
42: const displayTitle = title || titles[variant]
43:
44: // Generate unique ID for this instance
45: const uniqueId = `disclaimer-${Math.random().toString(36).slice(2, 9)}`
46: ---
47:
48:
49:
50: {
51: collapsible ? (
52:
53:
60:
61:
62:
63:
64:
Important disclaimer
65:
66:
67:
68:
69:
70:
71:
{displayTitle}
72:
73: {context} It does not constitute legal, HR, or professional advice and should not be relied on as a substitute for advice specific to your business, workforce, or circumstances.
74:
75:
89:
90:
91:
92: ) : (
93:
94:
95:
96:
97:
98:
99:
100:
{displayTitle}
101:
102:
103: {context} It does not constitute legal, HR, or professional advice and should not be relied on as a substitute for advice specific to your business, workforce, or circumstances.
104:
105:
119:
120:
121: )
122: }
123:
124:
125:
126:
````
## File: src/components/DownloadableChecklist.astro
````astro
1: ---
2: /**
3: * DownloadableChecklist - Printable compliance checklist component
4: *
5: * Creates a printable/downloadable checklist for NSW LSL compliance
6: * Users can print directly from browser or save as PDF
7: */
8:
9: interface Props {
10: title?: string
11: description?: string
12: background?: 'primary' | 'white'
13: }
14:
15: const {
16: title = 'NSW Long Service Leave Compliance Checklist',
17: description = "Print or save this checklist to ensure you're meeting all NSW LSL compliance requirements.",
18: background = 'white',
19: } = Astro.props
20:
21: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
22: ---
23:
24:
25:
26:
27:
FREE CHECKLIST
28:
{title}
29:
{description}
30:
31:
32:
37:
38: Print / Save as PDF
39:
40:
41:
42:
43:
44:
45:
46:
NSW Long Service Leave Compliance Checklist
47:
RosterElf | www.rosterelf.com | Downloaded:
48:
49:
50:
51:
52:
53:
54:
59:
60: Initial Setup
61:
62:
63:
64:
65:
66:
Review NSW Long Service Leave Act 1955
67:
Understand the baseline legal requirements for NSW employers
68:
69:
70:
71:
72:
73:
74:
Check for portable scheme coverage
75:
Determine if any employees work in construction, cleaning, or community services
76:
77:
78:
79:
80:
81:
82:
Register with Long Service NSW (if applicable)
83:
Required for employers in portable scheme industries
84:
85:
86:
87:
88:
89:
90:
Create LSL policy document
91:
Document your organization's LSL procedures and provide to employees
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
106:
107: Record Keeping & Tracking
108:
109:
110:
111:
112:
113:
Record employment start dates for all employees
114:
Essential for calculating continuous service periods
115:
116:
117:
118:
119:
120:
121:
Track LSL accrual for each employee
122:
Maintain accurate records of weeks accrued, taken, and balance
123:
124:
125:
126:
127:
128:
129:
Document any breaks in service
130:
Record unpaid leave, absences, and their impact on continuity
131:
132:
133:
134:
135:
136:
137:
Set up LSL tracking system
138:
Use payroll software or spreadsheet to monitor entitlements
139:
140:
141:
142:
143:
144:
145:
Provide LSL statements to employees annually
146:
Inform employees of their accrued LSL balance at least once per year
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158: Ongoing Compliance
159:
160:
161:
162:
163:
164:
Review LSL entitlements at 5, 7, and 10-year milestones
165:
Proactively check eligibility thresholds for pro-rata and full entitlements
166:
167:
168:
169:
170:
171:
172:
Process LSL requests within reasonable timeframe
173:
Respond to LSL applications and negotiate timing considering business needs
174:
175:
176:
177:
178:
179:
180:
Calculate LSL payments correctly
181:
Pay at ordinary rate, including relevant loadings for casuals
182:
183:
184:
185:
186:
187:
188:
Include LSL in payroll reconciliation
189:
Verify LSL accruals and payments during regular payroll audits
190:
191:
192:
193:
194:
195:
196:
Pay superannuation on LSL payments
197:
LSL is ordinary time earnings (OTE) requiring super contributions
198:
199:
200:
201:
202:
203:
204:
Lodge quarterly levy payments (portable schemes)
205:
For construction, cleaning, and community services employers
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
220:
221: Termination & Payout
222:
223:
224:
225:
226:
227:
Calculate LSL payout on termination
228:
Include in final pay for employees with 5+ years service
229:
230:
231:
232:
233:
234:
235:
Verify pro-rata eligibility criteria
236:
Check termination reason qualifies for pro-rata payment (5-10 years)
237:
238:
239:
240:
241:
242:
243:
Include LSL payout in payslip and separation certificate
244:
Document LSL payment clearly for employee records
245:
246:
247:
248:
249:
250:
251:
Retain LSL records for 7 years post-termination
252:
Maintain comprehensive records as required by NSW legislation
253:
254:
255:
256:
257:
258:
259:
260:
261: Checklist Progress
262: 0 of 23 completed
263:
264:
267:
268:
269:
270:
271:
272:
318:
319:
````
## File: src/components/ElectricalAwardCalculator.astro
````astro
1: ---
2: // Electrical Award Rate Calculator Component
3: // Data from 2025 Electrical, Electronic and Communications Contracting Award rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: 'Grade 1': 25.99,
11: 'Grade 2': 26.49,
12: 'Grade 3': 27.34,
13: 'Grade 4': 28.19,
14: 'Grade 5': 29.74,
15: 'Grade 6': 30.63,
16: 'Grade 7': 32.3,
17: 'Grade 8': 33.86,
18: 'Grade 9': 34.52,
19: 'Grade 10': 37.17,
20: },
21: casual: {
22: 'Grade 1': 32.49,
23: 'Grade 2': 33.11,
24: 'Grade 3': 34.18,
25: 'Grade 4': 35.24,
26: 'Grade 5': 37.18,
27: 'Grade 6': 38.29,
28: 'Grade 7': 40.38,
29: 'Grade 8': 42.33,
30: 'Grade 9': 43.15,
31: 'Grade 10': 46.46,
32: },
33: }
34:
35: // Fixed award rules - non-editable, shows how the award works
36: const awardRules = [
37: {
38: id: 'ordinary',
39: name: 'Ordinary hours (Mon-Sat)',
40: days: ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'],
41: startTime: '07:00',
42: endTime: '17:00',
43: penaltyMultiplier: { permanent: 1.0, casual: 1.25 },
44: },
45: {
46: id: 'sunday',
47: name: 'Sunday',
48: days: ['SUN'],
49: startTime: '00:00',
50: endTime: '00:00',
51: penaltyMultiplier: { permanent: 2.0, casual: 2.5 },
52: },
53: {
54: id: 'publicholiday',
55: name: 'Public holiday',
56: days: ['HOL'],
57: startTime: '00:00',
58: endTime: '00:00',
59: penaltyMultiplier: { permanent: 2.5, casual: 3.125 },
60: },
61: ]
62: ---
63:
64:
65:
66:
AWARD RATE ESTIMATOR
67:
See how RosterElf interprets the Electrical Award
68:
69: This is an educational example showing how the Electrical Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification grade,
70: employment type, and shift times.
71:
72:
73:
74:
75:
76:
77:
78:
79:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
80:
official Fair Work pay guide
86: and consulting your Award obligations.
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
Employment type
97:
98:
99:
100:
101:
105: Full-time / Part-time
106: Casual (includes 25% loading)
107:
108:
109:
110:
111:
112:
113:
Classification grade
114:
115:
116:
117:
118:
122: Grade 5 – Qualified electrician (tradesperson)
123: Grade 1 – Entry-level, labourer, trades assistant
124: Grade 2 – General duties, semi-skilled
125: Grade 3 – Intermediate tradesperson
126: Grade 4 – Skilled tradesperson
127: Grade 6 – Advanced tradesperson
128: Grade 7 – Special class tradesperson
129: Grade 8 – Technical specialist
130: Grade 9 – Technical supervisor
131: Grade 10 – Advanced technical/supervisory
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
Base ordinary rate
143:
Mon-Sat, standard hours
144:
145:
146:
147: $
148: 29.74
149: /hr
150:
151:
152:
153:
154:
155:
156:
157:
158:
159: Electrical Award penalty rates
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171: Example weekly cost (38 hours)
172:
173:
174:
175:
176:
177:
178:
179: Example total:
180: $1,130.12
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
Example only - not for payroll use
191:
This is a demonstration of how RosterElf calculates award-compliant rates.
192:
193:
194:
201: Important: Read all before using this calculator
202:
203:
204:
205:
206:
207:
208:
209:
The actual cost for your employees will depend on:
210:
211:
212: Their specific classification grade and employment type
213: Actual hours worked and shift times
214: Any additional allowances (licence, leading hand, tool, travel), overtime, or enterprise agreement provisions
215: Current award rates (which change annually in July)
216:
217:
218:
For accurate payroll calculations, always:
219:
220:
221:
222: Verify current rates with the official Fair Work pay guide
228:
229: Confirm your employees' correct award coverage and classification
230: Use award interpretation software or consult a payroll professional
231: Review your specific enterprise agreement (if applicable)
232:
233:
234:
Do not rely on this example for actual wage payments.
235:
236:
237:
238:
239:
240:
241:
242:
243:
244:
Stop calculating award rates manually
245:
Let RosterElf handle award compliance automatically
246:
247:
248: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
249: the work for you.
250:
251:
252:
253:
254:
255:
256:
257:
258:
259: No credit card required
260:
261:
262:
263: Full access
264:
265:
266:
267: 24/7 support
268:
269:
270:
271:
272:
273:
274:
How RosterElf automates award calculations
275:
276:
277:
278:
279:
283:
Create pay templates
284:
285: Create pay templates for each classification grade by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
286: each shift based on the employee's classification, shift timing, and employment type.
287:
288:
Award interpretation →
289:
290:
291:
292:
293:
297:
Define rate rules
298:
299: Configure when different penalty rates apply (Sundays, public holidays). The system automatically detects which rate to use based on shift times and days.
300:
301:
Penalty rates guide →
302:
303:
304:
305:
306:
310:
Auto-apply to shifts
311:
312: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
313:
314:
Payroll integration →
315:
316:
317:
318:
319:
320:
Learn more:
321:
328:
329:
330:
331:
332:
333:
557:
558:
````
## File: src/components/EmploymentLawCrossReference.astro
````astro
1: ---
2: import { MapPin, Scale, DollarSign, ArrowRight } from 'lucide-astro'
3:
4: interface Props {
5: currentSlug: string
6: maxItems?: number
7: background?: 'white' | 'primary'
8: }
9:
10: const { currentSlug, maxItems = 6, background = 'primary' } = Astro.props
11:
12: // All employment law pages data
13: const allEmploymentLawPages = [
14: {
15: slug: 'sa-long-service-leave',
16: title: 'SA Long Service Leave',
17: description: '13 weeks after 10 years, pro-rata at 7 years, payment calculations',
18: category: 'Long Service Leave',
19: state: 'SA',
20: href: '/guides/employment-law/sa-long-service-leave',
21: icon: MapPin,
22: },
23: {
24: slug: 'victoria-long-service-leave',
25: title: 'Victoria Long Service Leave',
26: description: '7-year eligibility, progressive entitlement, portable schemes',
27: category: 'Long Service Leave',
28: state: 'VIC',
29: href: '/guides/employment-law/victoria-long-service-leave',
30: icon: MapPin,
31: },
32: {
33: slug: 'qld-long-service-leave',
34: title: 'QLD Long Service Leave',
35: description: '10-year entitlement, 7-year pro-rata, QLeave portable schemes',
36: category: 'Long Service Leave',
37: state: 'QLD',
38: href: '/guides/employment-law/qld-long-service-leave',
39: icon: MapPin,
40: },
41: {
42: slug: 'victoria-workers-compensation',
43: title: "Victoria Workers' Compensation",
44: description: 'WorkCover claims, weekly payments (PIAWE), treatment expenses',
45: category: "Workers' Compensation",
46: state: 'VIC',
47: href: '/guides/employment-law/victoria-workers-compensation',
48: icon: Scale,
49: },
50: {
51: slug: 'workers-compensation-sa',
52: title: "SA Workers' Compensation",
53: description: 'ReturnToWorkSA claims, income support, return to work planning',
54: category: "Workers' Compensation",
55: state: 'SA',
56: href: '/guides/employment-law/workers-compensation-sa',
57: icon: Scale,
58: },
59: {
60: slug: 'nsw-payroll-tax',
61: title: 'NSW Payroll Tax',
62: description: '$1.2M threshold, 5.45% rate, grouping rules, monthly compliance',
63: category: 'Payroll Tax',
64: state: 'NSW',
65: href: '/guides/employment-law/nsw-payroll-tax',
66: icon: DollarSign,
67: },
68: {
69: slug: 'victoria-payroll-tax',
70: title: 'Victoria Payroll Tax',
71: description: '$3M threshold, 4.85% rate, regional rate (1.2125%), surcharges',
72: category: 'Payroll Tax',
73: state: 'VIC',
74: href: '/guides/employment-law/victoria-payroll-tax',
75: icon: DollarSign,
76: },
77: ]
78:
79: // Filter out current page and limit items
80: const filteredPages = allEmploymentLawPages.filter((page) => page.slug !== currentSlug).slice(0, maxItems)
81: ---
82:
83:
84:
85:
86:
87:
RELATED GUIDES
88:
Other employment law guides
89:
Explore other state-based guides for long service leave, workers' compensation, and payroll tax
90:
91:
92:
93:
118:
119:
````
## File: src/components/FAQSection.astro
````astro
1: ---
2: import { Plus, Minus } from 'lucide-astro'
3:
4: interface FAQ {
5: question: string
6: answer: string
7: }
8:
9: interface Category {
10: name: string
11: faqs: FAQ[]
12: }
13:
14: interface Props {
15: chip?: string
16: title?: string
17: description?: string
18: categories: Category[]
19: background?: 'primary' | 'white'
20: }
21:
22: const { chip = 'FAQ', title = 'Frequently asked questions', description, categories, background = 'primary' } = Astro.props
23:
24: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
25: ---
26:
27:
28:
29:
30:
{chip}
31:
{title}
32: {description &&
{description}
}
33:
34:
35:
36:
37:
38:
39: {
40: categories.map((category, index) => (
41:
42:
50: {category.name}
51:
52:
53: ))
54: }
55:
56:
57:
58:
59: {
60: categories.map((category, catIndex) => (
61:
62: {(category.faqs || []).map((faq, faqIndex) => (
63:
64:
70:
71:
72: {faq.question}
73:
74:
77:
78: ))}
79:
80: ))
81: }
82:
83:
84:
85:
86:
133:
134:
````
## File: src/components/FastFoodAwardCalculator.astro
````astro
1: ---
2: // Fast Food Award Rate Calculator Component
3: // Data from 2025 Fast Food Industry Award 2020 [MA000003] rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: 'Level 1': 26.55,
11: 'Level 2': 28.12,
12: 'Level 3': 28.9,
13: },
14: casual: {
15: 'Level 1': 33.19,
16: 'Level 2': 35.15,
17: 'Level 3': 36.13,
18: },
19: }
20:
21: // Fixed award rules - non-editable, shows how the Fast Food Award works
22: // Note: Sunday rates vary by classification level
23: const awardRules = [
24: {
25: id: 'ordinary',
26: name: 'Ordinary hours (Mon-Fri, 7am-10pm)',
27: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
28: startTime: '07:00',
29: endTime: '22:00',
30: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
31: },
32: {
33: id: 'evening',
34: name: 'Evening (Mon-Fri, 10pm-midnight)',
35: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
36: startTime: '22:00',
37: endTime: '00:00',
38: penaltyMultiplier: { permanent: 1.1, casual: 1.35 },
39: },
40: {
41: id: 'latenight',
42: name: 'Late night (Mon-Fri, midnight-6am)',
43: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
44: startTime: '00:00',
45: endTime: '06:00',
46: penaltyMultiplier: { permanent: 1.15, casual: 1.4 },
47: },
48: {
49: id: 'saturday',
50: name: 'Saturday (all day)',
51: days: ['SAT'],
52: startTime: '00:00',
53: endTime: '00:00',
54: penaltyMultiplier: { permanent: 1.25, casual: 1.5 },
55: },
56: {
57: id: 'sunday',
58: name: 'Sunday (varies by level)',
59: days: ['SUN'],
60: startTime: '00:00',
61: endTime: '00:00',
62: // Level 1: 125%/150%, Level 2/3: 150%/175%
63: penaltyMultiplier: {
64: 'Level 1': { permanent: 1.25, casual: 1.5 },
65: 'Level 2': { permanent: 1.5, casual: 1.75 },
66: 'Level 3': { permanent: 1.5, casual: 1.75 },
67: },
68: },
69: {
70: id: 'publicholiday',
71: name: 'Public holiday',
72: days: ['HOL'],
73: startTime: '00:00',
74: endTime: '00:00',
75: penaltyMultiplier: { permanent: 2.25, casual: 2.5 },
76: },
77: ]
78: ---
79:
80:
81:
82:
AWARD RATE ESTIMATOR
83:
See how RosterElf interprets the Fast Food Award
84:
85: This is an educational example showing how the Fast Food Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
86: employment type, and shift times.
87:
88:
89:
90:
91:
92:
93:
94:
95:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
96:
official Fair Work pay guide
99: and consulting your Award obligations.
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
Employment type
110:
111:
112:
113:
114:
118: Full-time / Part-time
119: Casual (includes 25% loading)
120:
121:
122:
123:
124:
125:
126:
Classification level
127:
128:
129:
130:
131:
135: Level 1 – Crew member (entry-level duties)
136: Level 2 – Trained crew (some independence)
137: Level 3 – Experienced crew (in charge of 2+ persons)
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
Base ordinary rate
149:
Mon-Fri, standard hours
150:
151:
152:
153: $
154: 26.55
155: /hr
156:
157:
158:
159:
160:
161:
162:
163:
164:
165: Fast Food Award penalty rates
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177: Example weekly cost (38 hours)
178:
179:
180:
181:
182:
183:
184:
185: Example total:
186: $1,089.00
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
Example only - not for payroll use
197:
This is a demonstration of how RosterElf calculates award-compliant rates.
198:
199:
200:
207: Important: Read all before using this calculator
208:
209:
210:
211:
212:
213:
214:
215:
The actual cost for your employees will depend on:
216:
217:
218: Their specific classification level and employment type
219: Actual hours worked and shift times
220: Any additional allowances, overtime, or enterprise agreement provisions
221: Current award rates (which change annually in July)
222: Junior rates if the employee is under 21
223:
224:
225:
For accurate payroll calculations, always:
226:
227:
228:
229: Verify current rates with the official Fair Work pay guide
235:
236: Confirm your employees' correct award coverage and classification
237: Use award interpretation software or consult a payroll professional
238: Review your specific enterprise agreement (if applicable)
239:
240:
241:
Do not rely on this example for actual wage payments.
242:
243:
244:
245:
246:
247:
248:
249:
250:
251:
Stop calculating penalty rates manually
252:
Let RosterElf handle award compliance automatically
253:
254:
255: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
256: the work for you.
257:
258:
259:
260:
261:
262:
263:
264:
265:
266: No credit card required
267:
268:
269:
270: Full access
271:
272:
273:
274: 24/7 support
275:
276:
277:
278:
279:
280:
281:
How RosterElf automates award calculations
282:
283:
284:
285:
286:
290:
Create pay templates
291:
292: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
293: each shift based on the employee's classification, shift timing, and employment type.
294:
295:
Award interpretation →
296:
297:
298:
299:
300:
304:
Define rate rules
305:
306: Configure when different penalty rates apply (late nights, weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
307:
308:
Penalty rates guide →
309:
310:
311:
312:
313:
317:
Auto-apply to shifts
318:
319: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
320:
321:
Payroll integration →
322:
323:
324:
325:
326:
327:
Learn more:
328:
335:
336:
337:
338:
339:
340:
583:
584:
````
## File: src/components/FeatureAccordionTabs.astro
````astro
1: ---
2: import { ChevronDown, CalendarCog, Clock, Scale, Wallet, Users, MessageSquare, BarChart3 } from 'lucide-astro'
3:
4: interface AccordionItem {
5: title: string
6: description: string
7: link: string
8: linkText?: string
9: image: string
10: }
11:
12: interface Tab {
13: id: string
14: name: string
15: icon: typeof CalendarCog
16: items: AccordionItem[]
17: }
18:
19: interface Props {
20: background?: 'primary' | 'white'
21: chip?: string
22: title?: string
23: tabs?: Tab[]
24: sectionId?: string
25: }
26:
27: const { background = 'white', chip = 'All-in-one platform', title = 'One workforce platform — built for Australian teams', tabs: customTabs, sectionId = 'feature-accordion-section' } = Astro.props
28: // Light green background #f4faf6 for primary, white for white
29: const bgStyle = background === 'primary' ? 'background-color: #f4faf6;' : ''
30: const textClass = 'text-gray-900'
31: const chipClass = 'chip-primary'
32: const navBgClass = 'border-gray-200 bg-white'
33:
34: const defaultTabs: Tab[] = [
35: {
36: id: 'rostering',
37: name: 'Rostering',
38: icon: CalendarCog,
39: items: [
40: {
41: title: 'Create smart schedules in minutes',
42: description: 'Quickly create and share rosters from any device—save time and reduce last-minute chaos.',
43: link: '/features/rostering-software',
44: linkText: 'Explore rostering',
45: image: '/images/rostering-software-hero.webp',
46: },
47: {
48: title: 'Control your labour costs',
49: description: 'See wage costs in real-time as you build rosters. Stay on budget before you publish.',
50: link: '/features/rostering-software#labour-budgeting',
51: linkText: 'Explore labour budgeting',
52: image: '/images/payroll-labour-budgets.webp',
53: },
54: {
55: title: 'Fill shifts with the right people',
56: description: 'Perfect Match suggests available, qualified staff based on skills, availability and cost.',
57: link: '/features/rostering-software#auto-scheduling',
58: linkText: 'See Perfect Match',
59: image: '/images/auto-scheduling-hero.webp',
60: },
61: {
62: title: 'Easy, fast, and staff-friendly',
63: description: 'Staff get instant notifications and can view rosters, swap shifts and update availability from the app.',
64: link: '/features/rostering-software',
65: linkText: 'Explore rostering',
66: image: '/images/auto-scheduling-open-shifts.webp',
67: },
68: ],
69: },
70: {
71: id: 'time-attendance',
72: name: 'Time & Attendance',
73: icon: Clock,
74: items: [
75: {
76: title: 'GPS-verified clock-ins',
77: description: 'Know exactly when and where staff clock in with GPS location capture and photo verification.',
78: link: '/features/time-and-attendance',
79: linkText: 'View time tracking',
80: image: '/images/time-attendance-gps-verification.webp',
81: },
82: {
83: title: 'Eliminate buddy punching',
84: description: 'Photo ID and GPS ensure the right person clocks in at the right location.',
85: link: '/features/time-and-attendance',
86: linkText: 'See verification',
87: image: '/images/time-attendance-photo-verification.webp',
88: },
89: {
90: title: 'Real-time attendance tracking',
91: description: "See who's on shift, who's late, and who's missing—all from your dashboard.",
92: link: '/features/time-and-attendance',
93: linkText: 'View attendance',
94: image: '/images/time-attendance-live-dashboard.webp',
95: },
96: {
97: title: 'Timesheet approvals made simple',
98: description: 'Review and approve timesheets with one click. Flag exceptions automatically.',
99: link: '/features/time-and-attendance',
100: linkText: 'Explore timesheets',
101: image: '/images/time-attendance-payroll-timesheets.webp',
102: },
103: ],
104: },
105: {
106: id: 'award-interpretation',
107: name: 'Award Interpretation',
108: icon: Scale,
109: items: [
110: {
111: title: 'Automate penalty rates',
112: description: 'RosterElf calculates overtime, weekend rates and public holiday pay automatically.',
113: link: '/features/payroll-integration/award-interpretation',
114: linkText: 'View awards',
115: image: '/images/award-interpretation-penalty-rates.png',
116: },
117: {
118: title: 'Stay Fair Work compliant',
119: description: 'Built-in award rules ensure you pay correctly every time—no manual calculations.',
120: link: '/features/payroll-integration/award-interpretation',
121: linkText: 'Explore compliance',
122: image: '/images/award-interpretation-fair-work.png',
123: },
124: {
125: title: 'Support for 100+ awards',
126: description: "Hospitality, retail, healthcare, childcare and more—we've got your award covered.",
127: link: '/features/payroll-integration/award-interpretation',
128: linkText: 'See awards',
129: image: '/images/award-interpretation-100-awards.png',
130: },
131: {
132: title: 'Reduce payroll disputes',
133: description: 'Accurate pay builds trust. Staff see exactly how their pay is calculated.',
134: link: '/features/payroll-integration/award-interpretation',
135: linkText: 'View benefits',
136: image: '/images/award-interpretation-reduce-disputes.png',
137: },
138: ],
139: },
140: {
141: id: 'payroll',
142: name: 'Payroll',
143: icon: Wallet,
144: items: [
145: {
146: title: 'One-click Xero & MYOB export',
147: description: 'Send award-interpreted timesheets directly to your payroll software in seconds.',
148: link: '/features/payroll-integration',
149: linkText: 'See payroll',
150: image: '/images/payroll-integration-hero.webp',
151: },
152: {
153: title: 'Payroll-ready timesheets',
154: description: 'Hours, rates and allowances calculated and formatted—ready for payroll.',
155: link: '/features/payroll-integration',
156: linkText: 'View payroll',
157: image: '/images/time-attendance-payroll-timesheets.webp',
158: },
159: {
160: title: 'Eliminate manual data entry',
161: description: 'No more copying numbers between systems. Reduce errors and save hours.',
162: link: '/features/payroll-integration',
163: linkText: 'Explore integration',
164: image: '/images/payroll-no-data-entry.png',
165: },
166: {
167: title: 'Full audit trail',
168: description: 'Track every change to timesheets with complete history for compliance.',
169: link: '/features/payroll-integration',
170: linkText: 'View audit trail',
171: image: '/images/payroll-audit-trail.png',
172: },
173: ],
174: },
175: {
176: id: 'hr',
177: name: 'HR Tools',
178: icon: Users,
179: items: [
180: {
181: title: 'Digital onboarding & contracts',
182: description: 'Send contracts, policies and tax forms digitally so new starters complete everything before day one — no printing, chasing or filing.',
183: link: '/features/hr-software/employee-onboarding',
184: linkText: 'See onboarding',
185: image: '/images/hr-contracts.webp',
186: },
187: {
188: title: 'Certifications, policies & procedures',
189: description: 'Track required documents, expiries and acknowledgements so nothing slips through the cracks — built for Australian workplaces.',
190: link: '/features/hr-software/policy-management',
191: linkText: 'View policies',
192: image: '/images/hr-certifications.webp',
193: },
194: {
195: title: 'Leave & availability management',
196: description: 'Manage leave requests, balances and approvals in one system, synced with rosters to avoid clashes and short staffing.',
197: link: '/features/hr-software/leave-management-software',
198: linkText: 'Manage leave',
199: image: '/images/hr-leave-management.webp',
200: },
201: {
202: title: 'Centralised employee records',
203: description: 'Store employee details, documents, certifications and history securely in one place, always up to date and easy to access.',
204: link: '/features/hr-software',
205: linkText: 'Explore HR tools',
206: image: '/images/hr-employee-records.png',
207: },
208: ],
209: },
210: {
211: id: 'communication',
212: name: 'Communication',
213: icon: MessageSquare,
214: items: [
215: {
216: title: 'In-app team messaging',
217: description: 'Message individuals, teams or your whole workforce—all within RosterElf.',
218: link: '/features/communication',
219: linkText: 'View communication',
220: image: '/images/communication-hero.webp',
221: },
222: {
223: title: 'Shift notifications',
224: description: 'Staff get instant alerts for new rosters, shift changes and open shifts.',
225: link: '/features/communication',
226: linkText: 'See notifications',
227: image: '/images/communication-shift-notifications.png',
228: },
229: {
230: title: 'Announcements',
231: description: "Share important updates with read receipts so you know who's seen them.",
232: link: '/features/communication',
233: linkText: 'Explore messaging',
234: image: '/images/communication-team-chat.webp',
235: },
236: {
237: title: 'Keep work chat separate',
238: description: 'No more WhatsApp groups. Keep work communication professional and private.',
239: link: '/features/communication',
240: linkText: 'View features',
241: image: '/images/communication-tasks.webp',
242: },
243: ],
244: },
245: {
246: id: 'analytics',
247: name: 'Analytics',
248: icon: BarChart3,
249: items: [
250: {
251: title: 'Real-time labour insights',
252: description: 'See labour costs, hours worked and budget tracking at a glance.',
253: link: '/features/analytics',
254: linkText: 'See analytics',
255: image: '/images/roster-reporting-hero.webp',
256: },
257: {
258: title: 'Attendance reports',
259: description: 'Track late arrivals, no-shows and overtime patterns across your team.',
260: link: '/features/analytics',
261: linkText: 'View reports',
262: image: '/images/time-attendance-live-dashboard.webp',
263: },
264: {
265: title: 'Cost forecasting',
266: description: 'Predict wage costs before you publish rosters. Stay within budget.',
267: link: '/features/analytics',
268: linkText: 'Explore forecasting',
269: image: '/images/analytics-cost-forecasting.png',
270: },
271: {
272: title: 'HR & compliance reports',
273: description: 'Download reports for payroll, compliance audits or management reviews.',
274: link: '/features/analytics',
275: linkText: 'View compliance',
276: image: '/images/hr-employee-records.png',
277: },
278: ],
279: },
280: ]
281:
282: // Use custom tabs if provided, otherwise use defaults
283: const tabs = customTabs || defaultTabs
284: ---
285:
286:
287:
288:
289: {chip}
290:
291: {title}
292:
293:
294:
295:
296:
326:
327:
328:
329: {
330: tabs.map((tab, tabIndex) => (
331:
338:
339:
340: {tab.items.length === 1 ? (
341: /* Single item - no accordion, just static content */
342:
349: ) : (
350: /* Multiple items - full accordion */
351: tab.items.map((item, itemIndex) => (
352:
357:
358: {item.title}
359:
360:
361:
367:
368: ))
369: )}
370:
371:
372:
373: {tab.items.map((item, itemIndex) => (
374:
384: ))}
385:
386:
387:
388: ))
389: }
390:
391:
392:
393:
394:
516:
517:
````
## File: src/components/FeatureCategoriesSection.astro
````astro
1: ---
2: import { Calendar, Clock, Landmark, Users, MessageCircle, BarChart3, ArrowRight, Check } from 'lucide-astro'
3:
4: // Interface for custom category data
5: export interface FeatureCategory {
6: name: string
7: tagline: string
8: description: string
9: icon: 'Calendar' | 'Clock' | 'Landmark' | 'Users' | 'MessageCircle' | 'BarChart3'
10: href: string
11: features: string[]
12: }
13:
14: interface Props {
15: chip?: string
16: title?: string
17: description?: string
18: background?: 'white' | 'primary'
19: categories?: FeatureCategory[] | null
20: }
21:
22: const {
23: chip = 'FEATURE CATEGORIES',
24: title = 'Everything you need to manage your workforce',
25: description = 'Six powerful modules working together to simplify rostering, time tracking, payroll, HR, communication, and analytics.',
26: background = 'white',
27: categories,
28: } = Astro.props
29:
30: // Icon map for string-based icon resolution
31: const iconMap = {
32: Calendar,
33: Clock,
34: Landmark,
35: Users,
36: MessageCircle,
37: BarChart3,
38: }
39:
40: const defaultCategories: FeatureCategory[] = [
41: {
42: name: 'Employee rostering',
43: tagline: 'Build and manage rosters with ease',
44: description: 'Schedule the right staff, control labour costs, and stay compliant with AI-assisted rostering and live budget tracking.',
45: icon: 'Calendar',
46: href: '/features/rostering-software',
47: features: ['Auto-scheduling', 'Staff availability', 'Roster templates', 'Multi-site rosters', 'Break planning', 'Labour budgeting'],
48: },
49: {
50: name: 'Time & attendance',
51: tagline: 'Track attendance with precision',
52: description: 'GPS clock-ins, photo proof, and digital timesheets ensure every clock-in is verified and accurate.',
53: icon: 'Clock',
54: href: '/features/time-and-attendance',
55: features: ['GPS & geofencing', 'Tablet time clock', 'Live attendance dashboard', 'Photo proof clock-in'],
56: },
57: {
58: name: 'Payroll integration',
59: tagline: 'Export approved hours in one click',
60: description: 'Connect directly to Xero or MYOB. Automate award interpretation for overtime, penalties, and loadings.',
61: icon: 'Landmark',
62: href: '/features/payroll-integration',
63: features: ['Award interpretation', 'Xero integration', 'MYOB integration', 'Automated exports'],
64: },
65: {
66: name: 'HR Hub',
67: tagline: 'Manage the full employee lifecycle',
68: description: 'From onboarding and digital contracts to policies, certificates, and leave management — all in one place.',
69: icon: 'Users',
70: href: '/features/hr-software',
71: features: ['Digital onboarding', 'Employment contracts', 'Certificate tracking', 'Workwise AI', 'Leave management'],
72: },
73: {
74: name: 'Communication',
75: tagline: 'Keep everyone on the same page',
76: description: 'Built-in chat, shift messages, and announcements simplify team communication across all your locations.',
77: icon: 'MessageCircle',
78: href: '/features/communication',
79: features: ['Team chat app', 'Shift notes', 'Staff notifications', 'Shift swaps', 'Newsfeed'],
80: },
81: {
82: name: 'Analytics',
83: tagline: 'Unlock instant visibility',
84: description: 'Real-time insights across rosters, wages, attendance, leave activity, HR compliance, and more.',
85: icon: 'BarChart3',
86: href: '/features/analytics',
87: features: ['Real-time roster insights', 'Labour cost reports', 'Attendance tracking', 'Compliance reporting'],
88: },
89: ]
90:
91: // Use custom categories if provided, otherwise use defaults
92: const featureCategories = categories || defaultCategories
93:
94: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
95: ---
96:
97:
98:
99:
100:
{chip}
101:
{title}
102:
{description}
103:
104:
105:
136:
137:
````
## File: src/components/FeatureGuidesSection.astro
````astro
1: ---
2: import { ArrowRight, BookOpen } from 'lucide-astro'
3: import { getGuidesForFeature, type GuideInfo } from '../data/guideMappings'
4:
5: interface Props {
6: currentFeature: string
7: background?: 'white' | 'primary'
8: }
9:
10: const { currentFeature, background = 'primary' } = Astro.props
11:
12: const relatedGuides = getGuidesForFeature(currentFeature)
13:
14: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
15: ---
16:
17: {
18: relatedGuides.length > 0 && (
19:
20:
21:
22:
HOW-TO GUIDES
23:
Learn how to get the most from this feature
24:
Step-by-step guides to help you master these workflows
25:
26:
27:
44:
45:
46: Browse all guides
47:
48:
49:
50:
51: )
52: }
````
## File: src/components/FeatureSection.astro
````astro
1: ---
2: interface Props {
3: chip: string
4: title: string
5: subChip: string
6: subtitle: string
7: paragraphs: string[]
8: ctaText?: string
9: ctaHref?: string
10: image: string
11: imageAlt: string
12: background?: 'white' | 'primary'
13: imagePosition?: 'left' | 'right'
14: }
15:
16: const { chip, title, subChip, subtitle, paragraphs, ctaText, ctaHref, image, imageAlt, background = 'white', imagePosition = 'left' } = Astro.props
17:
18: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
19: const flexDirection = imagePosition === 'left' ? 'lg:flex-row' : 'lg:flex-row-reverse'
20: ---
21:
22:
23:
24:
25: {chip}
26:
{title}
27:
28:
29:
30:
31:
32:
33:
{subChip}
34:
{subtitle}
35:
36:
37: {paragraphs.map((p) =>
{p}
)}
38:
39:
40: {
41: ctaText && ctaHref && (
42:
43: {ctaText} →
44:
45: )
46: }
47:
48:
49:
50:
````
## File: src/components/FeaturesGrid.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import type { ComponentType } from 'astro/types'
4:
5: interface FeatureItem {
6: title: string
7: description: string
8: icon: ComponentType
9: href?: string
10: }
11:
12: interface Props {
13: chip?: string
14: title: string
15: description?: string
16: items: FeatureItem[]
17: columns?: 3 | 4
18: background?: 'white' | 'primary'
19: }
20:
21: const { chip = 'KEY FEATURES', title, description, items, columns = 4, background = 'white' } = Astro.props
22:
23: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
24: const iconBgClass = background === 'white' ? 'bg-primary-100' : 'bg-white'
25: const gridCols = columns === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
26: ---
27:
28:
29:
30:
31:
{chip}
32:
{title}
33: {description &&
{description}
}
34:
35:
36:
64:
65:
````
## File: src/components/FeatureTabsSection.astro
````astro
1: ---
2: interface Props {
3: background?: 'primary' | 'white'
4: }
5:
6: const { background = 'primary' } = Astro.props
7: // Light green background #f4faf6 for primary
8: const bgStyle = background === 'primary' ? 'background-color: #f4faf6;' : ''
9: const textClass = 'text-gray-900'
10: const subtextClass = 'text-gray-600'
11:
12: interface FeatureTab {
13: id: string
14: name: string
15: tagline: string
16: image: string
17: link: string
18: }
19:
20: const tabs: FeatureTab[] = [
21: {
22: id: 'rostering',
23: name: 'Rostering',
24: tagline: 'Roster the right people, at the right time, all across your business',
25: image: '/images/feature-tab-rostering.png',
26: link: '/features/rostering-software',
27: },
28: {
29: id: 'time-attendance',
30: name: 'Time & Attendance',
31: tagline: 'Track every hour with GPS-verified clock-ins and real-time attendance',
32: image: '/images/feature-tab-time-attendance.png',
33: link: '/features/time-and-attendance',
34: },
35: {
36: id: 'payroll',
37: name: 'Awards & Payroll',
38: tagline: 'Send accurate, award-compliant timesheets to Xero or MYOB in one click',
39: image: '/images/feature-tab-payroll.png',
40: link: '/features/payroll-integration',
41: },
42: {
43: id: 'hr',
44: name: 'HR',
45: tagline: 'Manage onboarding, leave and compliance in one secure place',
46: image: '/images/feature-tab-hr.png',
47: link: '/features/hr-software',
48: },
49: {
50: id: 'communication',
51: name: 'Communication',
52: tagline: 'Keep your team connected with secure workplace messaging',
53: image: '/images/feature-tab-communication.png',
54: link: '/features/communication',
55: },
56: {
57: id: 'analytics',
58: name: 'Analytics',
59: tagline: 'Monitor labour costs and attendance trends in real time',
60: image: '/images/feature-tab-analytics.png',
61: link: '/features/analytics',
62: },
63: ]
64: ---
65:
66:
67:
68:
69: All-in-one platform
70:
One workforce platform — built for Australian teams
71:
72:
73:
74:
75:
76: {
77: tabs.map((tab, index) => (
78:
92: {tab.name}
93:
94:
95: ))
96: }
97:
98:
99:
100:
101:
102: {
103: tabs.map((tab, index) => (
104:
122: ))
123: }
124:
125:
126:
127:
128:
223:
224:
````
## File: src/components/FeatureTabsSimple.astro
````astro
1: ---
2: import { CalendarCog, Clock, Scale, Wallet, Users, BarChart3 } from 'lucide-astro'
3:
4: interface TabItem {
5: title: string
6: description: string
7: link: string
8: linkText?: string
9: image: string
10: }
11:
12: interface Tab {
13: id: string
14: name: string
15: icon: typeof CalendarCog
16: item: TabItem
17: }
18:
19: interface Props {
20: background?: 'primary' | 'white'
21: chip?: string
22: title?: string
23: tabs: Tab[]
24: sectionId?: string
25: }
26:
27: const { background = 'primary', chip = 'WHY ROSTERELF', title = 'Solve the hardest workforce problems — without adding admin', tabs, sectionId = 'feature-tabs-simple-section' } = Astro.props
28:
29: const bgStyle = background === 'primary' ? 'background-color: #f4faf6;' : ''
30: const textClass = 'text-gray-900'
31: const chipClass = 'chip-primary'
32: const navBgClass = 'border-gray-200 bg-white'
33: ---
34:
35:
36:
37:
38: {chip}
39:
40: {title}
41:
42:
43:
44:
45:
75:
76:
77:
78: {
79: tabs.map((tab, tabIndex) => (
80:
87:
88:
95:
96:
97:
98:
99:
100:
101: ))
102: }
103:
104:
105:
106:
107:
167:
168:
````
## File: src/components/FitnessAwardCalculator.astro
````astro
1: ---
2: // Fitness Award Rate Calculator Component
3: // Data from 2025 Fitness Industry Award rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: 'Level 1': 24.28,
11: 'Level 2': 24.95,
12: 'Level 3': 26.7,
13: 'Level 4': 29.27,
14: 'Level 5': 32.34,
15: 'Level 6': 32.06,
16: 'Level 7': 33.31,
17: },
18: casual: {
19: 'Level 1': 30.35, // Mon-Fri: base × 1.25
20: 'Level 2': 31.19,
21: 'Level 3': 33.38,
22: 'Level 4': 36.59,
23: 'Level 5': 40.43,
24: 'Level 6': 40.08,
25: 'Level 7': 41.64,
26: },
27: }
28:
29: // Fixed award rules - non-editable, shows how the award works
30: const awardRules = [
31: {
32: id: 'ordinary-weekday',
33: name: 'Ordinary hours (Mon-Fri)',
34: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
35: startTime: '05:00',
36: endTime: '21:00',
37: penaltyMultiplier: { permanent: 1.0, casual: 1.25 },
38: },
39: {
40: id: 'saturday',
41: name: 'Saturday',
42: days: ['SAT'],
43: penaltyMultiplier: { permanent: 1.25, casual: 1.3 },
44: },
45: {
46: id: 'sunday',
47: name: 'Sunday',
48: days: ['SUN'],
49: penaltyMultiplier: { permanent: 1.5, casual: 1.3 },
50: },
51: {
52: id: 'publicholiday',
53: name: 'Public holiday',
54: days: ['HOL'],
55: penaltyMultiplier: { permanent: 2.5, casual: 1.3 },
56: },
57: ]
58: ---
59:
60:
61:
62:
AWARD RATE ESTIMATOR
63:
See how RosterElf interprets the Fitness Award
64:
65: This is an educational example showing how the Fitness Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
66: employment type, and shift times.
67:
68:
69:
70:
71:
72:
73:
74:
75:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
76:
official Fair Work pay guide
79: and consulting your Award obligations.
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
Employment type
90:
91:
92:
93:
94:
98: Full-time / Part-time
99: Casual (Mon-Fri: 25% loading, Sat/Sun/PH: 30% loading)
100:
101:
102:
103:
104:
105:
106:
Classification level
107:
108:
109:
110:
111:
115: Level 1 – Receptionist, Gym Attendant
116: Level 2 – Fitness Instructor, Group Fitness Teacher
117: Level 3 – Personal Trainer, Qualified Instructor
118: Level 4 – Senior Instructor, Program Coordinator
119: Level 5 – Supervisor, Fitness Manager
120: Level 6 – Senior Manager, Multi-site Manager
121: Level 7 – Operations Manager, General Manager
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
Base ordinary rate
133:
Mon-Fri, standard hours
134:
135:
136:
137: $
138: 24.28
139: /hr
140:
141:
142:
143:
144:
145:
146:
147:
148:
149: Fitness Award penalty rates
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161: Example weekly cost (38 hours)
162:
163:
164:
165:
166:
167:
168:
169: Example total:
170: $1,025.29
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
Example only - not for payroll use
181:
This is a demonstration of how RosterElf calculates award-compliant rates.
182:
183:
184:
191: Important: Read all before using this calculator
192:
193:
194:
195:
196:
197:
198:
199:
The actual cost for your employees will depend on:
200:
201:
202: Their specific classification level and employment type
203: Actual hours worked and shift times
204: Any additional allowances, overtime, or enterprise agreement provisions
205: Current award rates (which change annually in July)
206:
207:
208:
For accurate payroll calculations, always:
209:
210:
211:
212: Verify current rates with the official Fair Work pay guide
218:
219: Confirm your employees' correct award coverage and classification
220: Use award interpretation software or consult a payroll professional
221: Review your specific enterprise agreement (if applicable)
222:
223:
224:
Do not rely on this example for actual wage payments.
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
Stop calculating penalty rates manually
235:
Let RosterElf handle award compliance automatically
236:
237:
238: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
239: the work for you.
240:
241:
242:
243:
244:
245:
246:
247:
248:
249: No credit card required
250:
251:
252:
253: Full access
254:
255:
256:
257: 24/7 support
258:
259:
260:
261:
262:
263:
264:
How RosterElf automates award calculations
265:
266:
267:
268:
269:
273:
Create pay templates
274:
275: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
276: each shift based on the employee's classification, shift timing, and employment type.
277:
278:
Award interpretation →
279:
280:
281:
282:
283:
287:
Define rate rules
288:
289: Configure when different penalty rates apply (weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
290:
291:
Penalty rates guide →
292:
293:
294:
295:
296:
300:
Auto-apply to shifts
301:
302: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
303:
304:
Payroll integration →
305:
306:
307:
308:
309:
310:
📚 Learn more:
311:
318:
319:
320:
321:
322:
323:
524:
525:
````
## File: src/components/Footer.astro
````astro
1: ---
2: import { Facebook, Instagram, Twitter, Linkedin, Youtube, Star, ChevronDown } from 'lucide-astro'
3: import { ratingsData } from '../data/ratingsData'
4: import { footerLinks as footerLinksAU } from '../data/navigation-au'
5: import { footerLinks as footerLinksUK } from '../data/navigation-uk'
6:
7: interface Props {
8: country?: 'uk' | null
9: }
10:
11: const { country } = Astro.props
12:
13: // Select footer data based on country
14: const footerLinks = country === 'uk' ? footerLinksUK : footerLinksAU
15: const homeHref = country === 'uk' ? '/uk' : '/'
16:
17: // Icon mapping for social links
18: const socialIcons = { Facebook, Instagram, Twitter, Linkedin, Youtube }
19:
20: // NOTE: Hardcoded footerLinks removed - now using imported data from navigation-au.ts / navigation-uk.ts
21: ---
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
{footerLinks.tagline}
32:
33:
34:
35:
36: {
37: footerLinks.appStores.map((store) => (
38:
39:
40:
41: ))
42: }
43:
44:
45:
46:
47: {
48: footerLinks.social.map((social) => {
49: const iconName = social.name === 'X' ? 'Twitter' : social.name === 'LinkedIn' ? 'Linkedin' : social.name === 'YouTube' ? 'Youtube' : social.name
50: const IconComponent = socialIcons[iconName as keyof typeof socialIcons]
51: return (
52:
59: {IconComponent && }
60:
61: )
62: })
63: }
64:
65:
66:
67:
68:
69: {
70: footerLinks.columns.map((column, index) => (
71:
0 }]}>
72:
76:
85:
86: ))
87: }
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
{footerLinks.copyright}
98:
99:
100:
101:
108:
120:
121:
122:
123:
124: {country === 'uk' ? 'United Kingdom' : 'Australia (Global)'}
125:
126:
127:
192:
193:
194:
195:
196:
197:
198: {
199: footerLinks.utility.map((link: { href: string; label: string; target?: string }, index: number) => (
200: <>
201:
202: {link.label}
203:
204: {index < footerLinks.utility.length - 1 &&
· }
205: >
206: ))
207: }
208:
209:
210:
211: {
212: footerLinks.legal.map((link) => (
213: <>
214:
215: {link.label}
216:
217:
·
218: >
219: ))
220: }
221:
Privacy settings
222:
·
223:
Login
224:
225:
226:
227:
228:
229:
230:
271:
272:
````
## File: src/components/FreeToolsGrid.astro
````astro
1: ---
2: /**
3: * FreeToolsGrid - App Store Style Tool Discovery Component
4: *
5: * A beautiful, reusable component to display free tools with filtering and cross-linking.
6: * Use on any page to help users discover related tools.
7: *
8: * Props:
9: * - title: Section title (default: "Explore more free tools")
10: * - subtitle: Section subtitle (optional)
11: * - background: "white" | "primary" (default: "primary")
12: * - excludeHref: Exclude a specific tool by its href (useful for current page)
13: * - category: Filter to specific category ("ai" | "rostering" | "cost" | "compliance" | "all")
14: * - maxTools: Maximum number of tools to display (default: all)
15: * - showFilters: Show category filter tabs (default: false)
16: * - layout: "grid" | "carousel" (default: "grid")
17: * - compact: Use smaller cards (default: false)
18: */
19:
20: import { freeTools, toolCategories, getRelatedTools, type FreeTool } from '../data/freeToolsData'
21: import { ratingsData } from '../data/ratingsData'
22: import {
23: Calculator,
24: Calendar,
25: Bot,
26: ArrowRight,
27: Sparkles,
28: DollarSign,
29: Clock,
30: TrendingUp,
31: Percent,
32: Timer,
33: Coffee,
34: Shield,
35: AlertTriangle,
36: Briefcase,
37: FileSignature,
38: Star,
39: ChevronLeft,
40: ChevronRight,
41: } from 'lucide-astro'
42:
43: interface Props {
44: title?: string
45: subtitle?: string
46: background?: 'white' | 'primary'
47: excludeHref?: string // Optional - auto-detects current page if not provided
48: category?: 'ai' | 'rostering' | 'cost' | 'compliance' | 'all'
49: maxTools?: number
50: showFilters?: boolean
51: layout?: 'grid' | 'carousel'
52: compact?: boolean
53: prioritizeRelated?: boolean // Show same-category tools first (default: true)
54: }
55:
56: const {
57: title = 'Explore more free tools',
58: subtitle,
59: background = 'primary',
60: excludeHref,
61: category = 'all',
62: maxTools,
63: showFilters = false,
64: layout = 'grid',
65: compact = false,
66: prioritizeRelated = true,
67: } = Astro.props
68:
69: // Auto-detect current page if excludeHref not provided
70: const currentPath = excludeHref || Astro.url.pathname
71:
72: // Get the current tool to determine its category for related tools
73: const currentTool = freeTools.find((t) => t.href === currentPath)
74:
75: // Build filtered tools list
76: let filteredTools: FreeTool[]
77:
78: if (prioritizeRelated && currentTool && category === 'all') {
79: // Smart ordering: same category first, then other categories
80: const sameCategory = freeTools.filter((t) => t.category === currentTool.category && t.href !== currentPath)
81: const otherCategories = freeTools.filter((t) => t.category !== currentTool.category && t.href !== currentPath)
82: filteredTools = [...sameCategory, ...otherCategories]
83: } else {
84: // Standard filtering
85: filteredTools = freeTools.filter((t) => t.href !== currentPath)
86:
87: // Filter by category if specified
88: if (category !== 'all') {
89: filteredTools = filteredTools.filter((t) => t.category === category)
90: }
91: }
92:
93: // Limit number of tools
94: if (maxTools && maxTools > 0) {
95: filteredTools = filteredTools.slice(0, maxTools)
96: }
97:
98: // Generate unique ID for this instance (for carousel)
99: const instanceId = Math.random().toString(36).substring(7)
100:
101: // Icon mapping component
102: const iconMap: Record = {
103: Bot,
104: Calendar,
105: Calculator,
106: DollarSign,
107: Clock,
108: TrendingUp,
109: Percent,
110: Timer,
111: Coffee,
112: Shield,
113: AlertTriangle,
114: Briefcase,
115: FileSignature,
116: Sparkles,
117: }
118: ---
119:
120:
121:
122:
123:
124:
{title}
125: {subtitle &&
{subtitle}
}
126:
127:
128:
129: {
130: showFilters && (
131:
132: {toolCategories.map((cat) => (
133:
147: {cat.label}
148: ({cat.count})
149:
150: ))}
151:
152: )
153: }
154:
155:
156: {
157: layout === 'grid' && (
158:
216: )
217: }
218:
219:
220: {
221: layout === 'carousel' && (
222:
223: {/* Carousel Container */}
224:
275:
276: {/* Navigation Arrows */}
277:
283:
284:
285:
291:
292:
293:
294: {/* Dots indicator */}
295:
296: {Array.from({ length: Math.ceil(filteredTools.length / 3) }).map((_, i) => (
297:
303: ))}
304:
305:
306: )
307: }
308:
309:
310:
322:
323:
324:
325:
405:
406:
````
## File: src/components/FreeVsPaidComparison.astro
````astro
1: ---
2: /**
3: * FreeVsPaidComparison Component
4: *
5: * Side-by-side comparison of Free Tools vs RosterElf Full Suite
6: * Uses dynamic pricing from pricingData.ts
7: * Supports custom content for specific tool pages
8: */
9:
10: import { Check, X, ArrowRight, Star } from 'lucide-astro'
11: import TrialButton from './TrialButton.astro'
12: import { pricingRates, plans, annualDiscountPercent, freeTrialDays } from '../data/pricingData'
13:
14: interface FeatureItem {
15: text: string
16: included: boolean
17: }
18:
19: interface Props {
20: background?: 'white' | 'primary'
21: /** Custom heading - defaults to "Need more than free tools?" */
22: title?: string
23: /** Custom subtitle */
24: subtitle?: string
25: /** Title for the free tools card */
26: freeTitle?: string
27: /** Description under the free tools price */
28: freeDescription?: string
29: /** Custom features for the free side */
30: freeFeatures?: FeatureItem[]
31: /** Link for "Continue with Free" button */
32: continueLink?: string
33: /** Text for continue button */
34: continueText?: string
35: }
36:
37: const {
38: background = 'white',
39: title = 'Need more than free tools?',
40: subtitle = 'Upgrade to RosterElf for automated scheduling, award compliance, and payroll integration.',
41: freeTitle = 'Free Tools',
42: freeDescription = 'Perfect for quick calculations and one-off tasks',
43: freeFeatures: customFreeFeatures,
44: continueLink = '#tools-section',
45: continueText = 'Continue with Free',
46: } = Astro.props
47:
48: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
49:
50: // Get Full Suite plan details
51: const fullSuitePlan = plans.find((p) => p.id === 'fullSuite')!
52: const annualPrice = pricingRates.fullSuite.annual
53: const monthlyPrice = pricingRates.fullSuite.monthly
54:
55: // Default free features if not provided
56: const defaultFreeFeatures: FeatureItem[] = [
57: { text: '14 free tools', included: true },
58: { text: 'No signup required', included: true },
59: { text: 'Instant browser access', included: true },
60: { text: 'Basic calculators & estimators', included: true },
61: { text: 'Save & sync rosters', included: false },
62: { text: 'Award interpretation', included: false },
63: { text: 'Payroll integration', included: false },
64: { text: 'Team management', included: false },
65: ]
66:
67: const freeFeatures = customFreeFeatures || defaultFreeFeatures
68:
69: // Core features that Full Suite includes
70: const coreFeatures = ['Smart rostering & availability', 'Time & attendance (GPS & photo)', 'Award interpretation & penalties', 'Payroll export (Xero & MYOB)']
71:
72: // Combine core + Full Suite specific features
73: const proFeatures = [...coreFeatures.map((f) => ({ text: f, included: true })), ...fullSuitePlan.features.map((f) => ({ text: f, included: true }))]
74: ---
75:
76:
77:
78:
79:
{title}
80:
81: {subtitle}
82:
83:
84:
85:
86:
87:
88:
89:
{freeTitle}
90:
91: $0
92: forever
93:
94:
{freeDescription}
95:
96:
97:
98: {
99: freeFeatures.map((feature) => (
100:
101: {feature.included ? : }
102: {feature.text}
103:
104: ))
105: }
106:
107:
108:
112: {continueText}
113:
114:
115:
116:
117:
118:
119:
120:
121: MOST POPULAR
122:
123:
124:
125:
{fullSuitePlan.name}
126:
{fullSuitePlan.tagline}
127:
128:
129:
130:
131:
132: ${annualPrice.toFixed(2)}
133:
134: AUD
135:
136:
137: per employee per month
138: + GST, billed annually
139:
140:
141:
142:
143:
144:
145: Annual
146: Monthly
147:
148:
149: Save {annualDiscountPercent}%
150:
151:
152:
153:
{fullSuitePlan.description}
154:
155:
156:
157: {
158: proFeatures.map((feature) => (
159:
160:
161: {feature.text}
162:
163: ))
164: }
165:
166:
167:
168:
169:
No credit card required · {freeTrialDays}-day free trial
170:
171:
172:
173:
174:
184:
185:
186:
187:
````
## File: src/components/GeneralRetailAwardCalculator.astro
````astro
1: ---
2: // General Retail Award Rate Calculator Component
3: // Data from 2025/26 General Retail Industry Award rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: 'Level 1': 26.55,
11: 'Level 2': 27.6,
12: 'Level 3': 28.55,
13: 'Level 4': 29.61,
14: 'Level 5': 30.69,
15: 'Level 6': 32.06,
16: 'Level 7': 33.42,
17: 'Level 8': 35.64,
18: },
19: casual: {
20: 'Level 1': 33.19,
21: 'Level 2': 34.5,
22: 'Level 3': 35.69,
23: 'Level 4': 37.01,
24: 'Level 5': 38.36,
25: 'Level 6': 40.08,
26: 'Level 7': 41.78,
27: 'Level 8': 44.55,
28: },
29: }
30:
31: // Fixed award rules - non-editable, shows how the award works
32: const awardRules = [
33: {
34: id: 'ordinary',
35: name: 'Ordinary hours (Mon-Fri before 6pm)',
36: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
37: startTime: '00:00',
38: endTime: '18:00',
39: penaltyMultiplier: { permanent: 1.0, casual: 1.25 },
40: },
41: {
42: id: 'evening',
43: name: 'Evening (Mon-Fri after 6pm)',
44: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
45: startTime: '18:00',
46: endTime: '00:00',
47: penaltyMultiplier: { permanent: 1.25, casual: 1.5 },
48: },
49: {
50: id: 'saturday-before',
51: name: 'Saturday before 6pm',
52: days: ['SAT'],
53: startTime: '00:00',
54: endTime: '18:00',
55: penaltyMultiplier: { permanent: 1.25, casual: 1.5 },
56: },
57: {
58: id: 'saturday-after',
59: name: 'Saturday after 6pm',
60: days: ['SAT'],
61: startTime: '18:00',
62: endTime: '00:00',
63: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
64: },
65: {
66: id: 'sunday',
67: name: 'Sunday (all day)',
68: days: ['SUN'],
69: startTime: '00:00',
70: endTime: '00:00',
71: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
72: },
73: {
74: id: 'publicholiday',
75: name: 'Public holiday',
76: days: ['HOL'],
77: startTime: '00:00',
78: endTime: '00:00',
79: penaltyMultiplier: { permanent: 2.25, casual: 2.5 },
80: },
81: ]
82: ---
83:
84:
85:
86:
AWARD RATE ESTIMATOR
87:
See how RosterElf interprets the General Retail Award
88:
89: This is an educational example showing how the General Retail Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
90: employment type, and shift times.
91:
92:
93:
94:
95:
96:
97:
98:
99:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
100:
official Fair Work pay guide
103: and consulting your Award obligations.
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
Employment type
114:
115:
116:
117:
118:
122: Full-time / Part-time
123: Casual (includes 25% loading)
124:
125:
126:
127:
128:
129:
130:
Classification level
131:
132:
133:
134:
135:
139: Level 1 – Retail Assistant (entry level)
140: Level 2 – Experienced Retail Assistant
141: Level 3 – Senior Retail Assistant / Supervisor
142: Level 4 – Senior Supervisor
143: Level 5 – Department Manager
144: Level 6 – Store Manager (small to medium)
145: Level 7 – Store Manager (large)
146: Level 8 – Senior Store Manager
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
Base ordinary rate
158:
Mon-Fri, before 6pm
159:
160:
161:
162: $
163: 26.55
164: /hr
165:
166:
167:
168:
169:
170:
171:
172:
173:
174: General Retail Award penalty rates
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186: Example weekly cost (38 hours)
187:
188:
189:
190:
191:
192:
193:
194: Example total:
195: $1,089.90
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
Example only - not for payroll use
206:
This is a demonstration of how RosterElf calculates award-compliant rates.
207:
208:
209:
216: Important: Read all before using this calculator
217:
218:
219:
220:
221:
222:
223:
224:
The actual cost for your employees will depend on:
225:
226:
227: Their specific classification level and employment type
228: Actual hours worked and shift times
229: Any additional allowances, overtime, or enterprise agreement provisions
230: Current award rates (which change annually in July)
231:
232:
233:
For accurate payroll calculations, always:
234:
235:
236:
237: Verify current rates with the official Fair Work pay guide
243:
244: Confirm your employees' correct award coverage and classification
245: Use award interpretation software or consult a payroll professional
246: Review your specific enterprise agreement (if applicable)
247:
248:
249:
Do not rely on this example for actual wage payments.
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
Stop calculating penalty rates manually
260:
Let RosterElf handle award compliance automatically
261:
262:
263: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
264: the work for you.
265:
266:
267:
268:
269:
270:
271:
272:
273:
274: No credit card required
275:
276:
277:
278: Full access
279:
280:
281:
282: 24/7 support
283:
284:
285:
286:
287:
288:
289:
How RosterElf automates award calculations
290:
291:
292:
293:
294:
298:
Create pay templates
299:
300: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
301: each shift based on the employee's classification, shift timing, and employment type.
302:
303:
Award interpretation →
304:
305:
306:
307:
308:
312:
Define rate rules
313:
314: Configure when different penalty rates apply (evenings, weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
315:
316:
Penalty rates guide →
317:
318:
319:
320:
321:
325:
Auto-apply to shifts
326:
327: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
328:
329:
Payroll integration →
330:
331:
332:
333:
334:
335:
Learn more:
336:
343:
344:
345:
346:
347:
348:
573:
574:
````
## File: src/components/GeoLocationModal.astro
````astro
1: ---
2: // Geo-Location Modal - prompts UK visitors to choose between AU and UK sites
3: ---
4:
5:
6:
7:
8:
9:
27:
You appear to be visiting from the UK
28:
We have a UK-specific site with local content and compliance information.
29:
30:
31:
32:
44:
45:
46:
47:
````
## File: src/components/GlossaryRelatedResources.astro
````astro
1: ---
2: import { ChevronRight } from 'lucide-astro'
3: import { readFile } from 'node:fs/promises'
4: import { join } from 'node:path'
5:
6: interface Article {
7: title: string
8: href: string
9: }
10:
11: interface Term {
12: term: string
13: slug?: string
14: }
15:
16: interface Props {
17: articles?: Article[]
18: terms?: Term[]
19: }
20:
21: /**
22: * Check if a blog post has a future premiere date
23: */
24: async function hasFuturePremiereDate(blogSlug: string): Promise {
25: try {
26: const filePath = join(process.cwd(), 'src/pages/blog', `${blogSlug}.astro`)
27: const content = await readFile(filePath, 'utf-8')
28:
29: // Extract premiere date from content
30: const premiereDateMatch = content.match(/premiereDate\s*=\s*['"]([^'"]+)['"]/)
31: if (!premiereDateMatch) return false
32:
33: const premiereDate = new Date(premiereDateMatch[1])
34: const now = new Date()
35:
36: // Compare dates at midnight
37: now.setHours(0, 0, 0, 0)
38: premiereDate.setHours(0, 0, 0, 0)
39:
40: return premiereDate > now
41: } catch {
42: // If file doesn't exist or can't be read, assume it's not a blog post or is published
43: return false
44: }
45: }
46:
47: /**
48: * Filter out articles with future premiere dates (blog posts only)
49: */
50: async function filterPublishedArticles(articles: Article[]): Promise {
51: const results = await Promise.all(
52: articles.map(async (article) => {
53: // Only check blog posts (href starts with /blog/)
54: if (!article.href.startsWith('/blog/')) return { article, isPublished: true }
55:
56: // Extract slug from href
57: const slug = article.href.replace('/blog/', '')
58: const isFuture = await hasFuturePremiereDate(slug)
59:
60: return { article, isPublished: !isFuture }
61: })
62: )
63:
64: return results.filter((r) => r.isPublished).map((r) => r.article)
65: }
66:
67: const { articles = [], terms = [] } = Astro.props
68:
69: // Filter out unpublished blog posts
70: const publishedArticles = await filterPublishedArticles(articles)
71:
72: const hasArticles = publishedArticles.length > 0
73: const hasTerms = terms.length > 0
74: const hasContent = hasArticles || hasTerms
75: ---
76:
77: {
78: hasContent && (
79:
80:
81:
Related resources
82:
Explore more guides and definitions
83:
84:
114:
115:
116: )
117: }
118:
119:
````
## File: src/components/GlossarySupportLinks.astro
````astro
1: ---
2: import { ChevronRight, BookOpen } from 'lucide-astro'
3:
4: interface SupportArticleLink {
5: title: string
6: href: string
7: description?: string
8: type?: 'setup' | 'integration' | 'advanced'
9: }
10:
11: interface Props {
12: articles: SupportArticleLink[]
13: maxLinks?: number
14: }
15:
16: const { articles = [], maxLinks = 4 } = Astro.props
17:
18: // Limit to maxLinks
19: const displayArticles = articles.slice(0, maxLinks)
20:
21: const hasArticles = displayArticles.length > 0
22: ---
23:
24: {
25: hasArticles && (
26:
27:
28:
29:
30:
31:
32:
33:
How to implement in RosterElf
34:
Step-by-step guides to set this up
35:
36:
37:
38:
55:
56: {articles.length > maxLinks &&
+{articles.length - maxLinks} more implementation guides
}
57:
58:
59: )
60: }
````
## File: src/components/Header.astro
````astro
1: ---
2: import {
3: ChevronDown,
4: Grid3x3,
5: Calendar,
6: Clock,
7: DollarSign,
8: Users,
9: MessageCircle,
10: Headphones,
11: BarChart,
12: CalendarCheck,
13: CalendarClock,
14: Smartphone,
15: ClipboardList,
16: UserCheck,
17: CalendarDays,
18: Building2,
19: AlertTriangle,
20: FileStack,
21: ListTodo,
22: Tablet,
23: MapPin,
24: Camera,
25: Award,
26: Heart,
27: Star,
28: Quote,
29: FileText,
30: GitCompare,
31: Puzzle,
32: Tag,
33: HelpCircle,
34: Sparkles,
35: Video,
36: BookOpen,
37: Phone,
38: Mail,
39: ThumbsUp,
40: Info,
41: FileUser,
42: UserPlus,
43: FileSignature,
44: ShieldCheck,
45: FolderOpen,
46: Briefcase,
47: LogOut,
48: GraduationCap,
49: TrendingUp,
50: Smile,
51: Bot,
52: Bell,
53: StickyNote,
54: Rss,
55: Activity,
56: Landmark,
57: ChartArea,
58: LayoutTemplate,
59: ChevronRight,
60: ArrowRight,
61: ChartColumnDecreasing,
62: Coffee,
63: ArrowLeftRight,
64: Hand,
65: Megaphone,
66: Calculator,
67: ClipboardCheck,
68: MessageSquare,
69: Newspaper,
70: CalendarMinus,
71: LogIn,
72: } from 'lucide-astro'
73: import TrialButton from './TrialButton.astro'
74: import { headerNavigation as navigationAU } from '../data/navigation-au'
75: import { headerNavigation as navigationUK } from '../data/navigation-uk'
76:
77: interface Props {
78: country?: 'uk' | null
79: }
80:
81: const { country } = Astro.props
82:
83: // Select navigation data based on country
84: const navigation = country === 'uk' ? navigationUK : navigationAU
85: const homeHref = country === 'uk' ? '/uk' : '/'
86: const pricingHref = country === 'uk' ? '/uk/pricing' : '/pricing'
87:
88: // Icon mapping helper
89: const iconMap = {
90: Grid3x3,
91: Calendar,
92: Clock,
93: DollarSign,
94: Users,
95: MessageCircle,
96: Headphones,
97: BarChart,
98: CalendarCheck,
99: CalendarClock,
100: Smartphone,
101: ClipboardList,
102: UserCheck,
103: CalendarDays,
104: Building2,
105: AlertTriangle,
106: FileStack,
107: ListTodo,
108: Tablet,
109: MapPin,
110: Camera,
111: Award,
112: Heart,
113: Star,
114: Quote,
115: FileText,
116: GitCompare,
117: Puzzle,
118: Tag,
119: HelpCircle,
120: Sparkles,
121: Video,
122: BookOpen,
123: Phone,
124: Mail,
125: ThumbsUp,
126: Info,
127: FileUser,
128: UserPlus,
129: FileSignature,
130: ShieldCheck,
131: FolderOpen,
132: Briefcase,
133: LogOut,
134: GraduationCap,
135: TrendingUp,
136: Smile,
137: Bot,
138: Bell,
139: StickyNote,
140: Rss,
141: Activity,
142: Landmark,
143: ChartArea,
144: LayoutTemplate,
145: ChartColumnDecreasing,
146: Coffee,
147: ArrowLeftRight,
148: Hand,
149: Megaphone,
150: Calculator,
151: ClipboardCheck,
152: MessageSquare,
153: Newspaper,
154: CalendarMinus,
155: LogIn,
156: }
157:
158: // Helper to render icon (Lucide or image)
159: const renderIcon = (icon: string, size: number, isImage?: boolean) => {
160: if (isImage) {
161: return { type: 'image', src: icon, size }
162: }
163: const Icon = iconMap[icon as keyof typeof iconMap]
164: return { type: 'icon', component: Icon, size }
165: }
166:
167: // Menu item type
168: type MenuItem = {
169: type: 'complex' | 'simple' | 'link'
170: label: string
171: icon?: string
172: href?: string
173: sections?: any[]
174: links?: any[]
175: class?: string
176: }
177:
178: // NOTE: Navigation data is now imported from src/data/navigation-au.ts or navigation-uk.ts
179: // The 'navigation' variable is set at the top of the frontmatter based on 'country' prop
180:
181: // Navigation data imported from data files (see imports above)
182:
183: // Filter out MYOB for UK
184: if (country === 'uk') {
185: // Remove MYOB from Payroll Integration section links
186: const payrollSection = navigation.product.sections.find((s) => s.id === 'payroll-integration')
187: if (payrollSection) {
188: payrollSection.links = payrollSection.links.filter((l: any) => l.label !== 'MYOB Integration')
189: payrollSection.highlight.description = 'Automate payroll by linking rosters with Xero.'
190: }
191: // Update Integrations menu: remove MYOB, point Google Maps to UK version
192: const integrationsMenu = navigation.simpleMenus[1]
193: if (integrationsMenu) {
194: integrationsMenu.links = integrationsMenu.links.filter((l) => l.label !== 'MYOB')
195: const googleMapsLink = integrationsMenu.links.find((l) => l.label === 'Google Maps')
196: if (googleMapsLink) {
197: googleMapsLink.href = '/uk/integrations/google-maps'
198: }
199: const googleCalendarLink = integrationsMenu.links.find((l) => l.label === 'Google Calendar')
200: if (googleCalendarLink) {
201: googleCalendarLink.href = '/uk/integrations/google-calendar'
202: }
203: const chatGptLink = integrationsMenu.links.find((l) => l.label === 'ChatGPT Atlas')
204: if (chatGptLink) {
205: chatGptLink.href = '/uk/integrations/chat-gpt'
206: }
207: }
208: }
209:
210: // Combined menu structure in order: Product, Why Us, Integrations, Pricing, Support, Resources
211: const nav2 = navigation.simpleMenus[2] as any
212: const nav2Type: 'link' | 'simple' = nav2?.href && !nav2?.links && !nav2?.columns ? 'link' : 'simple'
213: const menuItems: MenuItem[] = [
214: { type: 'complex', ...navigation.product },
215: { type: 'simple', ...navigation.simpleMenus[0] }, // Why Us
216: { type: 'simple', ...navigation.simpleMenus[1] }, // Integrations
217: { type: 'link', label: 'Pricing', icon: 'Tag', href: pricingHref },
218: { type: 'simple', ...navigation.simpleMenus[3] }, // Support
219: { type: nav2Type, ...navigation.simpleMenus[2] } as MenuItem, // Resources or Blog
220: { type: 'link', label: 'Login', icon: 'LogIn', href: 'https://rosterelf.net/', class: 'lg:hidden' },
221: ]
222:
223: // Standard icon sizes
224: const ICON_SIZE = 24
225: const CHEVRON_SIZE = 16
226: ---
227:
228:
492:
493:
668:
669:
````
## File: src/components/HeroAiTool.astro
````astro
1: ---
2: import { Check, ExternalLink, Bookmark, Sparkles } from 'lucide-astro'
3:
4: interface Feature {
5: text: string
6: }
7:
8: interface ChatMessage {
9: type: 'user' | 'ai'
10: text: string
11: template?: string
12: }
13:
14: interface Props {
15: chip?: string
16: title: string
17: subtitle: string
18: features: Feature[]
19: chatHeader: string
20: chatMessages?: ChatMessage[]
21: ctaText: string
22: ctaSubtext?: string
23: showBookmark?: boolean
24: }
25:
26: const { chip = 'FREE AI TOOL', title, subtitle, features, chatHeader, chatMessages, ctaText, ctaSubtext = 'Opens in ChatGPT (free account required)', showBookmark = true } = Astro.props
27:
28: const hasCustomChat = Astro.slots.has('chat')
29: ---
30:
31:
32:
33:
34:
35:
36:
{chip}
37:
38:
{subtitle}
39:
40: {
41: features.map((feature) => (
42:
43:
44: {feature.text}
45:
46: ))
47: }
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
{chatHeader}
59:
60:
61:
62: {
63: hasCustomChat ? (
64:
65: ) : (
66: chatMessages?.map((message) =>
67: message.type === 'user' ? (
68:
73: ) : (
74:
75:
76:
{message.text}
77: {message.template &&
{message.template}
}
78:
79:
80: )
81: )
82: )
83: }
84:
85:
86:
87:
88:
89:
90:
91:
92:
93: {
94: showBookmark && (
95:
96:
97: Bookmark this page
98:
99: )
100: }
101:
102:
103:
107: {ctaText}
108:
109:
110:
{ctaSubtext}
111:
112:
113:
114:
````
## File: src/components/HeroLite.astro
````astro
1: ---
2: /**
3: * HeroLite - Lightweight hero section for landing pages
4: *
5: * Usage:
6: *
7: *
8: * Custom headline here
9: *
10: */
11: import TrialButton from './TrialButton.astro'
12:
13: interface Props {
14: element: string
15: chip: string
16: description: string
17: }
18:
19: const { element, chip = '', description = '' } = Astro.props
20: ---
21:
22:
23:
24:
{chip}
25:
26:
27:
28:
{description}
29:
30:
31:
42:
43:
15-day free trial. No credit card required.
44:
45:
````
## File: src/components/HeroSection.astro
````astro
1: ---
2: import Breadcrumb from './Breadcrumb.astro'
3: import { Star, Check, CreditCard } from 'lucide-astro'
4: import TrialButton from './TrialButton.astro'
5:
6: interface BreadcrumbItem {
7: label: string
8: href: string
9: }
10:
11: interface BulletPoint {
12: text: string
13: }
14:
15: interface CTA {
16: text: string
17: href: string
18: variant: 'primary' | 'secondary'
19: target?: '_blank'
20: }
21:
22: interface Props {
23: breadcrumbs?: BreadcrumbItem[]
24: chip?: string
25: title: string
26: description?: string
27: bulletPoints?: BulletPoint[]
28: ctas?: CTA[]
29: showRating?: boolean
30: heroImage: string
31: heroImageAlt: string
32: showNoCreditCard?: boolean
33: hideBreadcrumbSpace?: boolean
34: }
35:
36: const { breadcrumbs, chip, title, description, bulletPoints, ctas, showRating = false, heroImage, heroImageAlt, showNoCreditCard = true, hideBreadcrumbSpace = false } = Astro.props
37: ---
38:
39:
40:
41:
42: {
43: breadcrumbs && breadcrumbs.length > 0 && (
44:
45:
46:
47: )
48: }
49: {!breadcrumbs && !hideBreadcrumbSpace &&
}
50: {!breadcrumbs && hideBreadcrumbSpace &&
}
51:
52:
53:
54:
55:
56: {chip &&
{chip} }
57:
58:
59:
60:
61:
62:
63:
64:
65: {description &&
{description}
}
66:
67: {
68: bulletPoints && bulletPoints.length > 0 && (
69:
70: {bulletPoints.map((point) => (
71:
72:
73:
74:
75: {point.text}
76:
77: ))}
78:
79: )
80: }
81:
82:
83:
101: {
102: showNoCreditCard && (
103:
104:
105: No credit card required
106:
107: )
108: }
109:
110:
111:
112: {
113: showRating && (
114:
115:
116:
117: {Array.from({ length: 5 }).map(() => (
118:
119: ))}
120:
121:
4.8 stars, 1,570 ratings
122:
123:
124: Best-rated rostering & HR software on{' '}
125:
126: Xero
127: {' '}
128: and{' '}
129:
135: Google
136:
137:
138:
139: )
140: }
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
````
## File: src/components/HospitalityAwardCalculator.astro
````astro
1: ---
2: // Hospitality Award Rate Calculator Component
3: // Data from 2025 Hospitality Industry (General) Award rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: Introductory: 24.28,
11: 'Level 1': 24.95,
12: 'Level 2': 25.85,
13: 'Level 3': 26.7,
14: 'Level 4': 28.12,
15: 'Level 5': 29.88,
16: 'Level 6': 30.68,
17: 'Managerial staff – hotel': 30.73,
18: },
19: casual: {
20: Introductory: 30.35,
21: 'Level 1': 31.19,
22: 'Level 2': 32.31,
23: 'Level 3': 33.38,
24: 'Level 4': 35.15,
25: 'Level 5': 37.35,
26: 'Level 6': 38.35,
27: 'Managerial staff – hotel': 38.41,
28: },
29: }
30:
31: // Fixed award rules - non-editable, shows how the award works
32: const awardRules = [
33: {
34: id: 'ordinary',
35: name: 'Ordinary hours (Mon-Fri)',
36: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
37: startTime: '07:00',
38: endTime: '19:00',
39: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
40: },
41: {
42: id: 'evening',
43: name: 'Evening rate (Mon-Fri)',
44: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
45: startTime: '19:00',
46: endTime: '00:00',
47: penaltyMultiplier: { permanent: 1.25, casual: 1.5 },
48: },
49: {
50: id: 'latenight',
51: name: 'Late night (Mon-Fri)',
52: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
53: startTime: '00:00',
54: endTime: '07:00',
55: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
56: },
57: {
58: id: 'saturday',
59: name: 'Saturday (all day)',
60: days: ['SAT'],
61: startTime: '00:00',
62: endTime: '00:00',
63: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
64: },
65: {
66: id: 'sunday',
67: name: 'Sunday (all day)',
68: days: ['SUN'],
69: startTime: '00:00',
70: endTime: '00:00',
71: penaltyMultiplier: { permanent: 1.75, casual: 2.0 },
72: },
73: {
74: id: 'publicholiday',
75: name: 'Public holiday',
76: days: ['HOL'],
77: startTime: '00:00',
78: endTime: '00:00',
79: penaltyMultiplier: { permanent: 2.5, casual: 2.75 },
80: },
81: ]
82: ---
83:
84:
85:
86:
AWARD RATE ESTIMATOR
87:
See how RosterElf interprets the Hospitality Award
88:
89: This is an educational example showing how the Hospitality Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
90: employment type, and shift times.
91:
92:
93:
94:
95:
96:
97:
98:
99:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
100:
official Fair Work pay guide
103: and consulting your Award obligations.
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
Employment type
114:
115:
116:
117:
118:
122: Full-time / Part-time
123: Casual (includes 25% loading)
124:
125:
126:
127:
128:
129:
130:
Classification level
131:
132:
133:
134:
135:
139: Level 1 – Food & Beverage, Guest Service, Kitchen Attendant
140: Introductory
141: Level 2 – Cook Grade 1, Clerical, Front Office
142: Level 3 – Cook Grade 2, Skilled Attendants
143: Level 4 – Tradesperson Grade 3
144: Level 5 – Supervisors, Tradesperson Grade 4
145: Level 6 – Cook Tradesperson Grade 5
146: Managerial staff – hotel
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
Base ordinary rate
158:
Mon-Fri, standard hours
159:
160:
161:
162: $
163: 24.95
164: /hr
165:
166:
167:
168:
169:
170:
171:
172:
173:
174: Hospitality Award penalty rates
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186: Example weekly cost (38 hours)
187:
188:
189:
190:
191:
192:
193:
194: Example total:
195: $1,025.29
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
Example only - not for payroll use
206:
This is a demonstration of how RosterElf calculates award-compliant rates.
207:
208:
209:
216: Important: Read all before using this calculator
217:
218:
219:
220:
221:
222:
223:
224:
The actual cost for your employees will depend on:
225:
226:
227: Their specific classification level and employment type
228: Actual hours worked and shift times
229: Any additional allowances, overtime, or enterprise agreement provisions
230: Current award rates (which change annually in July)
231:
232:
233:
For accurate payroll calculations, always:
234:
235:
236:
237: Verify current rates with the official Fair Work pay guide
243:
244: Confirm your employees' correct award coverage and classification
245: Use award interpretation software or consult a payroll professional
246: Review your specific enterprise agreement (if applicable)
247:
248:
249:
Do not rely on this example for actual wage payments.
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
Stop calculating penalty rates manually
260:
Let RosterElf handle award compliance automatically
261:
262:
263: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
264: the work for you.
265:
266:
267:
268:
269:
270:
271:
272:
273:
274: No credit card required
275:
276:
277:
278: Full access
279:
280:
281:
282: 24/7 support
283:
284:
285:
286:
287:
288:
289:
How RosterElf automates award calculations
290:
291:
292:
293:
294:
298:
Create pay templates
299:
300: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
301: each shift based on the employee's classification, shift timing, and employment type.
302:
303:
Award interpretation →
304:
305:
306:
307:
308:
312:
Define rate rules
313:
314: Configure when different penalty rates apply (evenings, weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
315:
316:
Penalty rates guide →
317:
318:
319:
320:
321:
325:
Auto-apply to shifts
326:
327: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
328:
329:
Payroll integration →
330:
331:
332:
333:
334:
335:
Learn more:
336:
343:
344:
345:
346:
347:
348:
573:
574:
````
## File: src/components/IconExample.astro
````astro
1: ---
2: // Example component showing Lucide icon usage
3: import { ChevronDown, Menu, X, ArrowRight, Calendar, Clock, Users, BarChart, MessageCircle, Settings, FileText, CheckCircle, AlertCircle, Info } from 'lucide-astro'
4: ---
5:
6:
7:
8: Navigation Icons
9:
10:
11:
12: ChevronDown
13:
14:
15:
16: Menu
17:
18:
19:
20: Close
21:
22:
26:
27:
28:
29:
30: Feature Icons
31:
32:
33:
34: Calendar
35:
36:
37:
38: Clock
39:
40:
41:
42: Users
43:
44:
45:
46: Analytics
47:
48:
49:
50: Chat
51:
52:
53:
54: Documents
55:
56:
57:
58:
59:
60: Status Icons
61:
62:
63:
64: Success
65:
66:
70:
71:
72: Info
73:
74:
75:
76:
77:
78: Interactive Example
79:
80: Click me
81:
82:
83:
84:
85:
86: Dropdown Example
87:
88: Select option
89:
90:
91:
92:
93:
94:
````
## File: src/components/IndustryGrid.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import type { ComponentType } from 'astro/types'
4:
5: interface IndustryItem {
6: name: string
7: description: string
8: icon: ComponentType
9: href?: string
10: isActive?: boolean
11: }
12:
13: interface Props {
14: chip?: string
15: title?: string
16: description?: string
17: items: IndustryItem[]
18: background?: 'white' | 'primary'
19: }
20:
21: const {
22: chip = 'INDUSTRIES WE SERVE',
23: title = 'Rostering software for every industry',
24: description = 'Purpose-built workforce management for Australian businesses across hospitality, healthcare, retail, and more.',
25: items,
26: background = 'white',
27: } = Astro.props
28:
29: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
30: const iconBgClass = background === 'white' ? 'bg-primary-100' : 'bg-white'
31: ---
32:
33:
34:
35:
36:
{chip}
37:
{title}
38:
{description}
39:
40:
41:
70:
71:
````
## File: src/components/IndustryServiceSchema.astro
````astro
1: ---
2: /**
3: * IndustryServiceSchema - Generates schema.org Service markup for industry pages
4: *
5: * This component adds structured data to help search engines understand
6: * that RosterElf offers workforce management services to specific industries.
7: */
8:
9: interface Props {
10: name: string // e.g., "Hospitality", "Retail", "Healthcare"
11: description: string // Page meta description
12: url: string // Canonical URL path e.g., "/industries/hospitality"
13: image?: string // Hero image URL
14: }
15:
16: const { name, description, url, image } = Astro.props
17:
18: const serviceSchema = {
19: '@context': 'https://schema.org',
20: '@type': 'Service',
21: name: `RosterElf for ${name}`,
22: description: description,
23: url: `https://www.rosterelf.com${url}`,
24: ...(image && { image: `https://www.rosterelf.com${image}` }),
25: provider: {
26: '@type': 'Organization',
27: name: 'RosterElf',
28: url: 'https://www.rosterelf.com',
29: logo: 'https://www.rosterelf.com/images/logos/rosterelf-logo.svg',
30: },
31: areaServed: {
32: '@type': 'Country',
33: name: 'Australia',
34: },
35: serviceType: 'Workforce Management Software',
36: category: 'Business Software',
37: hasOfferCatalog: {
38: '@type': 'OfferCatalog',
39: name: 'RosterElf Plans',
40: itemListElement: [
41: {
42: '@type': 'Offer',
43: itemOffered: {
44: '@type': 'Service',
45: name: 'RosterElf Rostering & HR Software',
46: },
47: price: '4.00',
48: priceCurrency: 'AUD',
49: priceSpecification: {
50: '@type': 'UnitPriceSpecification',
51: price: '4.00',
52: priceCurrency: 'AUD',
53: unitText: 'per active employee per month',
54: },
55: },
56: ],
57: },
58: }
59: ---
60:
61:
````
## File: src/components/LatestInsightsSection.astro
````astro
1: ---
2: /**
3: * LatestInsightsSection - Display latest blog posts and insights on homepage
4: *
5: * Usage (default - one from each category):
6: *
7: *
8: * Usage (focused on single blog category - shows 3 from that category):
9: *
10: *
11: * Premiere dates are respected - only published posts are shown.
12: */
13: import { ArrowRight, Clock, BookOpen, Newspaper } from 'lucide-astro'
14:
15: interface ContentItem {
16: title: string
17: excerpt: string
18: href: string
19: image: string
20: category: string
21: readTime?: string
22: }
23:
24: interface Props {
25: title?: string
26: subtitle?: string
27: background?: 'white' | 'primary'
28: focusBlogCategory?: string // If set, shows 3 blogs from this single category
29: focusInsightCategory?: string // If set, shows 3 insights from this single category
30: }
31:
32: const { title = 'Latest insights', subtitle = 'Tips and guides for Australian businesses', background = 'primary', focusBlogCategory, focusInsightCategory } = Astro.props
33:
34: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
35: const cardBg = 'bg-white'
36:
37: // Target categories to display (one post from each) - used when no focusBlogCategory
38: const targetCategories = ['HR & Compliance', 'Rostering & Scheduling', 'Payroll & Integrations']
39:
40: // Check if a post is published (premiere date has passed or doesn't exist)
41: function isPublished(premiereDate?: string): boolean {
42: if (!premiereDate) return true
43: const now = new Date()
44: const premiere = new Date(premiereDate)
45: now.setHours(0, 0, 0, 0)
46: premiere.setHours(0, 0, 0, 0)
47: return premiere <= now
48: }
49:
50: // Fetch all blog posts and extract metadata
51: const blogModules = import.meta.glob('../pages/blog/*.astro', { eager: true }) as Record
52:
53: // Process blogs: filter published, sort by date
54: const allBlogs = Object.entries(blogModules)
55: .filter(([_, mod]) => mod.articleMeta && isPublished(mod.premiereDate))
56: .map(([path, mod]) => ({
57: ...mod.articleMeta,
58: href: `/blog/${path.split('/').pop()?.replace('.astro', '')}`,
59: }))
60: .sort((a, b) => (b.dateISO || '').localeCompare(a.dateISO || ''))
61:
62: // Get blog posts based on mode
63: let blogPosts: ContentItem[]
64: if (focusBlogCategory) {
65: // Focus mode: get 3 most recent from single category
66: blogPosts = allBlogs.filter((post) => post.category === focusBlogCategory).slice(0, 3)
67: } else {
68: // Default mode: get most recent post from each target category
69: blogPosts = targetCategories.map((category) => allBlogs.find((post) => post.category === category)).filter((post): post is ContentItem => !!post)
70: }
71:
72: // All insights data with categories (sorted by datePublished within each category)
73: // Images only shown in this component, not on actual insight pages
74: const allInsightsData: (ContentItem & { datePublished: string })[] = [
75: // Workforce Management
76: {
77: title: 'Employee rostering best practices',
78: excerpt: 'Best practices for fair, compliant rosters that reduce costs, fatigue and turnover.',
79: href: '/insights/employee-rostering-australia',
80: image: '/images/stock/unsplash-1552581234-26160f608093.webp',
81: category: 'Workforce Management',
82: datePublished: '2025-01-01',
83: },
84: {
85: title: 'Employee timesheets: legal requirements',
86: excerpt: 'Legal requirements for recording hours, breaks, approvals and compliance.',
87: href: '/insights/employee-timesheets-australia',
88: image: '/images/stock/unsplash-1434626881859-194d67b2b86f.webp',
89: category: 'Workforce Management',
90: datePublished: '2025-01-01',
91: },
92: {
93: title: 'Employee onboarding compliance',
94: excerpt: 'Contracts, Fair Work requirements, tax, super and record-keeping rules.',
95: href: '/insights/employee-onboarding-australia',
96: image: '/images/stock/unsplash-1521737711867-e3b97375f902.webp',
97: category: 'Workforce Management',
98: datePublished: '2025-01-01',
99: },
100: // Trends
101: {
102: title: 'Shift patterns & fatigue management',
103: excerpt: 'Managing fatigue and wellbeing in shift-based workforces.',
104: href: '/insights/shift-patterns-fatigue-trends',
105: image: '/images/stock/unsplash-1540575467063-178a50c2df87.webp',
106: category: 'Trends',
107: datePublished: '2025-12-01',
108: },
109: {
110: title: 'Future of work in Australia',
111: excerpt: 'Trends shaping the future of work for Australian small businesses.',
112: href: '/insights/future-of-work-australia',
113: image: '/images/stock/unsplash-1497215842964-222b430dc094.webp',
114: category: 'Trends',
115: datePublished: '2025-01-01',
116: },
117: {
118: title: 'Labour market trends',
119: excerpt: 'Australian labour market trends and what they mean for SMEs.',
120: href: '/insights/labour-market-trends-australia',
121: image: '/images/stock/unsplash-1460925895917-afdab827c52f.webp',
122: category: 'Trends',
123: datePublished: '2025-01-01',
124: },
125: // Compliance & Awards
126: {
127: title: 'Award compliance for Australian small businesses',
128: excerpt: 'Understand pay rates, penalties, overtime, allowances and Fair Work obligations.',
129: href: '/insights/award-compliance-australian-small-businesses',
130: image: '/images/stock/unsplash-1454165804606-c3d57bc86b40.webp',
131: category: 'Compliance & Awards',
132: datePublished: '2025-01-01',
133: },
134: {
135: title: 'Modern awards explained',
136: excerpt: 'A practical guide to understanding modern awards for Australian SMEs.',
137: href: '/insights/modern-awards-explained',
138: image: '/images/stock/unsplash-1450101499163-c8848c66ca85.webp',
139: category: 'Compliance & Awards',
140: datePublished: '2025-01-01',
141: },
142: {
143: title: 'Payroll compliance (Australia)',
144: excerpt: 'Essential payroll compliance requirements for Australian businesses.',
145: href: '/insights/payroll-compliance-australia',
146: image: '/images/stock/unsplash-1554224155-6726b3ff858f.webp',
147: category: 'Compliance & Awards',
148: datePublished: '2025-01-01',
149: },
150: ]
151:
152: // Target insight categories (order determines display order) - used when no focusInsightCategory
153: const targetInsightCategories = ['Workforce Management', 'Trends', 'Compliance & Awards']
154:
155: // Get insights based on mode
156: let insights: ContentItem[]
157: if (focusInsightCategory) {
158: // Focus mode: get 3 most recent from single category
159: insights = allInsightsData
160: .filter((i) => i.category === focusInsightCategory)
161: .sort((a, b) => b.datePublished.localeCompare(a.datePublished))
162: .slice(0, 3)
163: } else {
164: // Default mode: get most recent insight from each target category
165: insights = targetInsightCategories
166: .map((category) => allInsightsData.filter((i) => i.category === category).sort((a, b) => b.datePublished.localeCompare(a.datePublished))[0])
167: .filter((insight): insight is ContentItem => !!insight)
168: }
169: ---
170:
171:
172:
173:
174:
INSIGHTS
175:
{title}
176: {subtitle &&
{subtitle}
}
177:
178:
179:
180:
181:
182:
183:
184: Blog
185:
186:
187:
188: E-guides
189:
190:
191:
192:
193:
194:
223:
224:
225:
252:
253:
254:
255:
272:
273:
````
## File: src/components/LiteYouTube.astro
````astro
1: ---
2: interface Props {
3: videoId: string
4: title: string
5: class?: string
6: }
7:
8: const { videoId, title, class: className = '' } = Astro.props
9: ---
10:
11:
12: VIDEO
19:
20:
21:
````
## File: src/components/LoginButton.astro
````astro
1: ---
2: /**
3: * LoginButton - Reusable CTA button for logging in
4: *
5: * Automatically sets tracking params:
6: * - landing: entry page name (from PageTracker)
7: * - exit: current page name (from PageTracker)
8: * - element: where the button is placed
9: */
10:
11: interface Props {
12: element: string
13: class?: string
14: variant?: 'primary' | 'secondary'
15: }
16:
17: const { element, class: className, variant = 'primary' } = Astro.props
18:
19: const primaryClass = 'bg-primary flex h-12.5 shrink-0 items-center justify-center gap-2 rounded px-8 py-3 text-base font-semibold text-white transition-all duration-300 hover:bg-black'
20: const secondaryClass = 'text-primary inline-flex items-center gap-2 font-semibold hover:underline'
21: const variantClass = variant === 'primary' ? primaryClass : secondaryClass
22: const finalClass = className ? `${variantClass} ${className}` : variantClass
23: ---
24:
25:
26: {variant === 'primary' ? 'Login to RosterElf' : 'Login'}
27:
28:
29:
````
## File: src/components/LogisticsAwardCalculator.astro
````astro
1: ---
2: // Logistics Award Rate Calculator Component
3: // Data from 2025 Road Transport and Distribution Award rates (MA000038)
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator, Truck } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: weekly: {
10: Transport: {
11: 'Grade 1': 25.65,
12: 'Grade 2': 26.27,
13: 'Grade 3': 26.57,
14: 'Grade 4': 27.04,
15: 'Grade 5': 27.37,
16: 'Grade 6': 27.68,
17: 'Grade 7': 28.09,
18: 'Grade 8': 28.9,
19: 'Grade 9': 29.39,
20: 'Grade 10': 30.12,
21: },
22: Distribution: {
23: 'Level 1': 26.57,
24: 'Level 2': 27.04,
25: 'Level 3': 28.09,
26: 'Level 4': 29.39,
27: },
28: },
29: casual: {
30: Transport: {
31: 'Grade 1': 32.06,
32: 'Grade 2': 32.84,
33: 'Grade 3': 33.21,
34: 'Grade 4': 33.8,
35: 'Grade 5': 34.21,
36: 'Grade 6': 34.6,
37: 'Grade 7': 35.11,
38: 'Grade 8': 36.13,
39: 'Grade 9': 36.74,
40: 'Grade 10': 37.65,
41: },
42: Distribution: {
43: 'Level 1': 33.21,
44: 'Level 2': 33.8,
45: 'Level 3': 35.11,
46: 'Level 4': 36.74,
47: },
48: },
49: }
50:
51: // Fixed award rules - non-editable, shows how the award works
52: const awardRules = [
53: {
54: id: 'ordinary',
55: name: 'Ordinary hours (Mon-Fri)',
56: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
57: startTime: '00:00',
58: endTime: '00:00',
59: penaltyMultiplier: { weekly: 1.0, casual: 1.0 },
60: },
61: {
62: id: 'saturday',
63: name: 'Saturday (all day)',
64: days: ['SAT'],
65: startTime: '00:00',
66: endTime: '00:00',
67: penaltyMultiplier: { weekly: 1.5, casual: 1.75 },
68: },
69: {
70: id: 'sunday',
71: name: 'Sunday (all day)',
72: days: ['SUN'],
73: startTime: '00:00',
74: endTime: '00:00',
75: penaltyMultiplier: { weekly: 2.0, casual: 2.25 },
76: },
77: {
78: id: 'publicholiday',
79: name: 'Public holiday',
80: days: ['HOL'],
81: startTime: '00:00',
82: endTime: '00:00',
83: penaltyMultiplier: { weekly: 2.5, casual: 2.75 },
84: },
85: ]
86: ---
87:
88:
89:
90:
AWARD RATE ESTIMATOR
91:
See how RosterElf interprets the Logistics Award
92:
93: This is an educational example showing how the Road Transport and Distribution Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on stream
94: (transport or distribution), classification level, employment type, and shift times.
95:
96:
97:
98:
99:
100:
101:
102:
103:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
104:
official Fair Work pay guide
107: and consulting your Award obligations. Calculator shows ordinary rates only—overtime rates differ (see overtime section below).
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
Employment type
118:
119:
120:
121:
122:
126: Weekly hire
127: Casual (includes 25% loading)
128:
129:
130:
131:
132:
133:
134:
Work stream
135:
136:
137:
138:
139:
143: Transport worker (drivers)
144: Distribution facility (warehouse)
145:
146:
147:
148:
149:
150:
151:
Classification level
152:
153:
154:
155:
156:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
Base ordinary rate
172:
Monday to Friday
173:
174:
175:
176: $
177: 25.65
178: /hr
179:
180:
181:
182:
183:
184:
185:
186:
187:
188: Logistics Award penalty rates
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200: Example weekly cost (38 hours)
201:
202:
203:
204:
205:
206:
207:
208: Example total:
209: $974.70
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
Example only - not for payroll use
220:
This is a demonstration of how RosterElf calculates award-compliant rates.
221:
222:
223:
230: Important: Read all before using this calculator
231:
232:
233:
234:
235:
236:
237:
238:
The actual cost for your employees will depend on:
239:
240:
241: Their specific classification level and employment type
242: Actual hours worked and shift times
243: Any additional allowances, overtime, or enterprise agreement provisions
244: Current award rates (which change annually in July)
245: Oil distribution workers have different calculation methods (see full rates table)
246:
247:
248:
For accurate payroll calculations, always:
249:
250:
251:
252: Verify current rates with the official Fair Work pay guide
258:
259: Confirm your employees' correct award coverage and classification
260: Use award interpretation software or consult a payroll professional
261: Review your specific enterprise agreement (if applicable)
262:
263:
264:
Do not rely on this example for actual wage payments.
265:
266:
267:
268:
269:
270:
271:
272:
273:
274:
Stop calculating penalty rates manually
275:
Let RosterElf handle award compliance automatically
276:
277:
278: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
279: the work for you.
280:
281:
282:
283:
284:
285:
286:
287:
288:
289: No credit card required
290:
291:
292:
293: Full access
294:
295:
296:
297: 24/7 support
298:
299:
300:
301:
302:
303:
304:
How RosterElf automates award calculations
305:
306:
307:
308:
309:
313:
Create pay templates
314:
315: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
316: each shift based on the employee's classification, shift timing, and employment type.
317:
318:
Award interpretation →
319:
320:
321:
322:
323:
327:
Define rate rules
328:
329: Configure when different penalty rates apply (Saturdays, Sundays, public holidays). The system automatically detects which rate to use based on shift times and days.
330:
331:
Penalty rates guide →
332:
333:
334:
335:
336:
340:
Auto-apply to shifts
341:
342: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
343:
344:
Payroll integration →
345:
346:
347:
348:
349:
350:
📚 Learn more:
351:
358:
359:
360:
361:
362:
363:
597:
598:
````
## File: src/components/LSLCalculator.astro
````astro
1: ---
2: /**
3: * LSLCalculator - NSW Long Service Leave Calculator Component
4: *
5: * Interactive calculator to determine LSL entitlements based on service period
6: * Calculates weeks owed and payment amount for NSW employees
7: *
8: * Props:
9: * - chip: Optional chip label (default: "FREE CALCULATOR")
10: * - title: Calculator title (default: "NSW long service leave calculator")
11: * - description: Calculator description
12: * - background: "primary" | "white" (default: "white")
13: */
14:
15: interface Props {
16: chip?: string
17: title?: string
18: description?: string
19: background?: 'primary' | 'white'
20: }
21:
22: const {
23: chip = 'FREE CALCULATOR',
24: title = 'NSW long service leave calculator',
25: description = "Calculate your long service leave entitlements based on years of continuous service. Enter your employment dates to see how much LSL you're owed.",
26: background = 'white',
27: } = Astro.props
28:
29: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
30: ---
31:
32:
33:
34:
35:
{chip}
36:
{title}
37:
{description}
38:
39:
40:
41:
42:
43:
Employment details
44:
45:
46: Employment start date
47:
53:
54:
55:
56: Employment end date (or leave blank if still employed)
57:
58:
59:
60:
61: Employment type
62:
63: Full-time
64: Part-time
65: Casual
66:
67:
68:
69:
70:
Ordinary weekly pay ($)
71:
80:
Enter gross weekly earnings (for casuals, include loading)
81:
82:
83:
84: Calculate LSL entitlement
85:
86:
87:
88:
89:
90:
Your LSL entitlement
91:
92:
93:
94:
95: Years of continuous service
96:
97:
98:
99:
100:
101: LSL weeks entitled
102: 8.67 weeks
103:
104:
105:
106:
107: Estimated LSL payment
108: $10,404
109:
110:
111:
112:
113:
114: Congratulations! You've reached the 10-year milestone and are entitled to 8.67 weeks of paid long service leave.
115:
116:
117:
118:
119:
120:
121: Next milestone:
122: At 15 years, you'll be entitled to an additional 4.33 weeks (13 weeks total).
123:
124:
125:
126:
127:
128:
129: Disclaimer: This calculator provides estimates based on the NSW Long Service Leave Act 1955. Actual entitlements may vary based on your specific employment agreement, portable
130: scheme coverage, or pro-rata eligibility. For official calculations, use the
131: NSW Government LSL calculator .
134:
135:
136:
137:
138:
139:
140:
141:
Understanding your LSL entitlement
142:
143:
144:
152:
First entitlement: 8.67 weeks (2 months) of paid leave
153:
154:
155:
163:
Pro-rata eligible if employment ends due to illness, dismissal, or other qualifying reasons
164:
165:
166:
174:
Additional 4.33 weeks at 15 years, then 4.33 weeks per 5 years thereafter
175:
176:
177:
178:
179:
180:
181:
````
## File: src/components/MailchimpNewsletter.astro
````astro
1: ---
2: interface Props {
3: title?: string
4: description?: string
5: background?: 'white' | 'primary'
6: }
7:
8: const {
9: title = 'Get notified about new features',
10: description = 'Subscribe to our newsletter to receive product updates, tips, and workforce management insights.',
11: background = 'primary',
12: } = Astro.props
13:
14: const backgroundClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
15: ---
16:
17:
18:
19:
{title}
20:
{description}
21:
42:
No spam. Unsubscribe anytime.
43:
44:
````
## File: src/components/MeatAwardCalculator.astro
````astro
1: ---
2: // Meat Award Rate Calculator Component
3: // Data from 2025 Meat Industry Award 2020 [MA000059] rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: MI1: 24.28,
11: MI2: 25.09,
12: MI3: 25.4,
13: MI4: 25.99,
14: MI5: 26.45,
15: MI6: 26.99,
16: MI7: 28.12,
17: MI8: 29.15,
18: },
19: casual: {
20: MI1: 30.35,
21: MI2: 31.36,
22: MI3: 31.75,
23: MI4: 32.49,
24: MI5: 33.06,
25: MI6: 33.74,
26: MI7: 35.15,
27: MI8: 36.44,
28: },
29: }
30:
31: // Fixed award rules - non-editable, shows how the Meat Award works
32: // Note: These are for meat processing establishments
33: const awardRules = [
34: {
35: id: 'ordinary',
36: name: 'Ordinary hours (Mon-Fri, 6am-8pm)',
37: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
38: startTime: '06:00',
39: endTime: '20:00',
40: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
41: },
42: {
43: id: 'afternoon',
44: name: 'Afternoon shift (2pm-midnight)',
45: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
46: startTime: '14:00',
47: endTime: '00:00',
48: penaltyMultiplier: { permanent: 1.15, casual: 1.4 },
49: },
50: {
51: id: 'night',
52: name: 'Night shift (after midnight)',
53: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
54: startTime: '00:00',
55: endTime: '06:00',
56: penaltyMultiplier: { permanent: 1.25, casual: 1.5 },
57: },
58: {
59: id: 'fixednight',
60: name: 'Fixed night shift (10pm-6am)',
61: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
62: startTime: '22:00',
63: endTime: '06:00',
64: penaltyMultiplier: { permanent: 1.3, casual: 1.55 },
65: },
66: {
67: id: 'saturday',
68: name: 'Saturday (all day)',
69: days: ['SAT'],
70: startTime: '00:00',
71: endTime: '00:00',
72: penaltyMultiplier: { permanent: 1.5, casual: 1.5 },
73: },
74: {
75: id: 'sunday',
76: name: 'Sunday (all day)',
77: days: ['SUN'],
78: startTime: '00:00',
79: endTime: '00:00',
80: penaltyMultiplier: { permanent: 2.0, casual: 2.0 },
81: },
82: {
83: id: 'publicholiday',
84: name: 'Public holiday',
85: days: ['HOL'],
86: startTime: '00:00',
87: endTime: '00:00',
88: penaltyMultiplier: { permanent: 2.0, casual: 2.0 },
89: },
90: ]
91: ---
92:
93:
94:
95:
AWARD RATE ESTIMATOR
96:
See how RosterElf interprets the Meat Award
97:
98: This is an educational example showing how the Meat Award penalty rates work for meat processing establishments. It demonstrates how RosterElf automatically calculates correct pay rates based on
99: classification level, employment type, and shift times.
100:
101:
102:
103:
104:
105:
106:
107:
108:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
109:
official Fair Work pay guide
112: and consulting your Award obligations.
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
Note: This calculator demonstrates meat processing establishment rates. Manufacturing and retail establishments have different rules for span of hours and
123: penalties.
See establishment type differences →
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
Employment type
134:
135:
136:
137:
138:
142: Full-time / Part-time
143: Casual (includes 25% loading)
144:
145:
146:
147:
148:
149:
150:
Classification level
151:
152:
153:
154:
155:
159: MI1 – Introductory level (entry-level duties)
160: MI2 – Level 2 (basic tasks)
161: MI3 – Level 3 (trained tasks)
162: MI4 – Level 4 (skilled tasks)
163: MI5 – Level 5 (advanced skills)
164: MI6 – Level 6 (specialist or supervisory)
165: MI7 – Level 7 (senior roles)
166: MI8 – Level 8 (highly skilled specialist)
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
Base ordinary rate
178:
Mon-Fri, standard hours
179:
180:
181:
182: $
183: 24.28
184: /hr
185:
186:
187:
188:
189:
190:
191:
192:
193:
194: Meat Award penalty rates (processing establishments)
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206: Example weekly cost (38 hours)
207:
208:
209:
210:
211:
212:
213:
214: Example total:
215: $1,089.00
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
Example only - not for payroll use
226:
This is a demonstration of how RosterElf calculates award-compliant rates.
227:
228:
229:
236: Important: Read all before using this calculator
237:
238:
239:
240:
241:
242:
243:
244:
The actual cost for your employees will depend on:
245:
246:
247: Their specific classification level and employment type
248: Actual hours worked and shift times
249: Which type of establishment (processing, manufacturing, or retail)
250: Any additional allowances, overtime, or enterprise agreement provisions
251: Current award rates (which change annually in July)
252: Junior rates if the employee is under 21
253:
254:
255:
For accurate payroll calculations, always:
256:
257:
258:
259: Verify current rates with the official Fair Work pay guide
265:
266: Confirm your employees' correct award coverage and classification
267: Use award interpretation software or consult a payroll professional
268: Review your specific enterprise agreement (if applicable)
269:
270:
271:
Do not rely on this example for actual wage payments.
272:
273:
274:
275:
276:
277:
278:
279:
280:
281:
Stop calculating penalty rates manually
282:
Let RosterElf handle award compliance automatically
283:
284:
285: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
286: the work for you.
287:
288:
289:
290:
291:
292:
293:
294:
295:
296: No credit card required
297:
298:
299:
300: Full access
301:
302:
303:
304: 24/7 support
305:
306:
307:
308:
309:
310:
311:
How RosterElf automates award calculations
312:
313:
314:
315:
316:
320:
Create pay templates
321:
322: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
323: each shift based on the employee's classification, shift timing, and employment type.
324:
325:
Award interpretation →
326:
327:
328:
329:
330:
334:
Define rate rules
335:
336: Configure when different penalty rates apply (late nights, weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
337:
338:
Penalty rates guide →
339:
340:
341:
342:
343:
347:
Auto-apply to shifts
348:
349: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
350:
351:
Payroll integration →
352:
353:
354:
355:
356:
357:
Learn more:
358:
365:
366:
367:
368:
369:
370:
596:
597:
````
## File: src/components/NursesAwardCalculator.astro
````astro
1: ---
2: // Nurses Award Rate Calculator Component
3: // Data from 2025/26 Nurses Award rates (MA000034)
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: nonAgedCare: {
10: permanent: {
11: 'EN PP1': 28.64,
12: 'EN PP3': 29.4,
13: 'EN PP5': 30.13,
14: 'RN L1 PP1': 30.64,
15: 'RN L1 PP4': 33.09,
16: 'RN L1 PP8': 36.82,
17: 'RN L2 PP1': 37.79,
18: 'RN L2 PP4': 39.7,
19: 'RN L3 PP1': 40.98,
20: 'RN L3 PP4': 43.21,
21: 'RN L4': 46.77,
22: 'RN L5': 47.19,
23: },
24: casual: {},
25: },
26: agedCare: {
27: permanent: {
28: EN: 37.09,
29: 'RN L1 (first year)': 38.07,
30: 'RN L1 (1-4 years)': 40.0,
31: 'RN L1 (>4 years)': 43.72,
32: 'RN L2 (first 3 years)': 47.34,
33: 'RN L2 (>3 years)': 49.48,
34: 'RN L3': 51.21,
35: 'RN L4': 59.19,
36: 'RN L5': 66.91,
37: },
38: casual: {},
39: },
40: }
41:
42: // Calculate casual rates (25% loading)
43: for (const sector of ['nonAgedCare', 'agedCare']) {
44: for (const level in ratesData[sector].permanent) {
45: ratesData[sector].casual[level] = Number((ratesData[sector].permanent[level] * 1.25).toFixed(2))
46: }
47: }
48:
49: // Fixed award rules - non-editable, shows how the award works
50: const awardRules = [
51: {
52: id: 'ordinary',
53: name: 'Ordinary hours (Mon-Fri)',
54: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
55: startTime: '07:00',
56: endTime: '18:00',
57: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
58: },
59: {
60: id: 'saturday',
61: name: 'Saturday (all day)',
62: days: ['SAT'],
63: startTime: '00:00',
64: endTime: '00:00',
65: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
66: },
67: {
68: id: 'sunday',
69: name: 'Sunday (all day)',
70: days: ['SUN'],
71: startTime: '00:00',
72: endTime: '00:00',
73: penaltyMultiplier: { permanent: 1.75, casual: 2.0 },
74: },
75: {
76: id: 'publicholiday',
77: name: 'Public holiday',
78: days: ['HOL'],
79: startTime: '00:00',
80: endTime: '00:00',
81: penaltyMultiplier: { permanent: 2.5, casual: 2.75 },
82: },
83: ]
84: ---
85:
86:
382:
383:
658:
659:
````
## File: src/components/OccupationSchema.astro
````astro
1: ---
2: /**
3: * OccupationSchema Component
4: * Generates schema.org Occupation markup for job description template pages
5: * This is for describing job roles/occupations, NOT for actual job postings
6: */
7:
8: interface Responsibility {
9: title: string
10: description: string
11: }
12:
13: interface Skill {
14: title: string
15: description: string
16: }
17:
18: interface Qualification {
19: name: string
20: required: boolean
21: description: string
22: }
23:
24: interface Props {
25: name: string // e.g., "Bartender"
26: description: string
27: responsibilities?: Responsibility[]
28: skills?: Skill[]
29: qualifications?: Qualification[]
30: industry?: string // e.g., "Hospitality"
31: employmentType?: string[] // e.g., ["FULL_TIME", "PART_TIME", "CASUAL"]
32: }
33:
34: const { name, description, responsibilities = [], skills = [], qualifications = [], industry, employmentType = ['FULL_TIME', 'PART_TIME'] } = Astro.props
35:
36: const schema = {
37: '@context': 'https://schema.org',
38: '@type': 'Occupation',
39: name: name,
40: description: description,
41: ...(responsibilities.length > 0 && {
42: responsibilities: responsibilities.map((r) => r.description).join('. '),
43: }),
44: ...(skills.length > 0 && {
45: skills: skills.map((s) => s.title).join(', '),
46: }),
47: ...(qualifications.length > 0 && {
48: qualifications: qualifications
49: .filter((q) => q.required)
50: .map((q) => q.name)
51: .join(', '),
52: }),
53: ...(industry && {
54: occupationalCategory: industry,
55: }),
56: occupationLocation: {
57: '@type': 'Country',
58: name: 'Australia',
59: },
60: estimatedSalary: {
61: '@type': 'MonetaryAmountDistribution',
62: name: 'Base salary',
63: currency: 'AUD',
64: duration: 'P1H',
65: description: 'Hourly rate based on applicable Modern Award',
66: },
67: }
68: ---
69:
70:
````
## File: src/components/OtherAuthors.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3:
4: interface Author {
5: name: string
6: role: string
7: photo: string
8: href: string
9: }
10:
11: interface Props {
12: authors: Author[]
13: title?: string
14: description?: string
15: background?: 'white' | 'primary'
16: }
17:
18: const { authors, title = 'Meet the other authors', description = 'Explore content from our other contributors.', background = 'white' } = Astro.props
19:
20: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
21: ---
22:
23:
24:
25:
26:
TEAM
27:
{title}
28:
{description}
29:
30:
31:
48:
49:
````
## File: src/components/PageTracker.astro
````astro
1: ---
2: /**
3: * PageTracker - Global component for tracking entry and current pages
4: *
5: * Access anywhere via:
6: * - window.RosterElf.entryPage - Full path: /features/rostering-software
7: * - window.RosterElf.entryPageName - Page name: rostering-software
8: * - window.RosterElf.currentPage - Full path of current page
9: * - window.RosterElf.currentPageName - Page name of current page
10: * - window.RosterElf.tracking - Tracking data (source, device, etc.)
11: */
12: ---
13:
14:
````
## File: src/components/PastoralAwardCalculator.astro
````astro
1: ---
2: // Pastoral Award Rate Calculator Component
3: // Data from 2025 Pastoral Award rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: FLH1: 24.28,
11: FLH2: 24.95,
12: FLH3: 25.29,
13: FLH4: 25.85,
14: FLH5: 26.3,
15: FLH6: 26.7,
16: FLH7: 28.12,
17: FLH8: 30.21,
18: },
19: casual: {
20: FLH1: 30.35,
21: FLH2: 31.19,
22: FLH3: 31.61,
23: FLH4: 32.31,
24: FLH5: 32.88,
25: FLH6: 33.38,
26: FLH7: 35.15,
27: FLH8: 37.76,
28: },
29: }
30:
31: // Fixed award rules - non-editable, shows how the award works
32: const awardRules = [
33: {
34: id: 'ordinary',
35: name: 'Ordinary hours (Mon-Sat)',
36: days: ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'],
37: startTime: '00:00',
38: endTime: '00:00',
39: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
40: },
41: {
42: id: 'overtime-mon-sat',
43: name: 'Overtime (Mon-Sat)',
44: days: ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'],
45: startTime: '00:00',
46: endTime: '00:00',
47: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
48: },
49: {
50: id: 'sunday-feeding',
51: name: 'Sunday - feeding & watering stock',
52: days: ['SUN'],
53: startTime: '00:00',
54: endTime: '00:00',
55: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
56: },
57: {
58: id: 'sunday-other',
59: name: 'Sunday - other work',
60: days: ['SUN'],
61: startTime: '00:00',
62: endTime: '00:00',
63: penaltyMultiplier: { permanent: 2.0, casual: 2.25 },
64: },
65: {
66: id: 'publicholiday',
67: name: 'Public holiday',
68: days: ['HOL'],
69: startTime: '00:00',
70: endTime: '00:00',
71: penaltyMultiplier: { permanent: 2.0, casual: 2.25 },
72: },
73: ]
74: ---
75:
76:
77:
78:
AWARD RATE ESTIMATOR
79:
See how RosterElf interprets the Pastoral Award
80:
81: This is an educational example showing how the Pastoral Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
82: employment type, and shift times.
83:
84:
85:
86:
87:
88:
89:
90:
91:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
92:
official Fair Work pay guide
95: and consulting your Award obligations.
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
Employment type
106:
107:
108:
109:
110:
114: Full-time / Part-time
115: Casual (includes 25% loading)
116:
117:
118:
119:
120:
121:
122:
Classification level
123:
124:
125:
126:
127:
131: FLH2 – Station hand (6-12 months exp)
132: FLH1 – Station hand (< 6 months exp)
133: FLH3 – Experienced farm hand
134: FLH4 – Advanced duties
135: FLH5 – Leading hand duties
136: FLH6 – Skilled operations
137: FLH7 – Supervisor level
138: FLH8 – Senior supervisor
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
Base ordinary rate
150:
Mon-Sat, ordinary hours
151:
152:
153:
154: $
155: 24.95
156: /hr
157:
158:
159:
160:
161:
162:
163:
164:
165:
166: Pastoral Award penalty rates
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178: Example weekly cost (38 hours)
179:
180:
181:
182:
183:
184:
185:
186: Example total:
187: $1,047.90
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
Example only - not for payroll use
198:
This is a demonstration of how RosterElf calculates award-compliant rates.
199:
200:
201:
208: Important: Read all before using this calculator
209:
210:
211:
212:
213:
214:
215:
216:
The actual cost for your employees will depend on:
217:
218:
219: Their specific classification level and employment type
220: Actual hours worked and shift times
221: Any additional allowances, overtime, or enterprise agreement provisions
222: Current award rates (which change annually in July)
223:
224:
225:
For accurate payroll calculations, always:
226:
227:
228:
229: Verify current rates with the official Fair Work pay guide
235:
236: Confirm your employees' correct award coverage and classification
237: Use award interpretation software or consult a payroll professional
238: Review your specific enterprise agreement (if applicable)
239:
240:
241:
Do not rely on this example for actual wage payments.
242:
243:
244:
245:
246:
247:
248:
249:
250:
251:
Stop calculating penalty rates manually
252:
Let RosterElf handle award compliance automatically
253:
254:
255: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
256: the work for you.
257:
258:
259:
260:
261:
262:
263:
264:
265:
266: No credit card required
267:
268:
269:
270: Full access
271:
272:
273:
274: 24/7 support
275:
276:
277:
278:
279:
280:
281:
How RosterElf automates award calculations
282:
283:
284:
285:
286:
290:
Create pay templates
291:
292: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
293: each shift based on the employee's classification, shift timing, and employment type.
294:
295:
Award interpretation →
296:
297:
298:
299:
300:
304:
Define rate rules
305:
306: Configure when different penalty rates apply (overtime, Sundays, public holidays). The system automatically detects which rate to use based on shift times and days.
307:
308:
Penalty rates guide →
309:
310:
311:
312:
313:
317:
Auto-apply to shifts
318:
319: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
320:
321:
Payroll integration →
322:
323:
324:
325:
326:
327:
Learn more:
328:
335:
336:
337:
338:
339:
340:
565:
566:
````
## File: src/components/PlumbingAwardCalculator.astro
````astro
1: ---
2: // Plumbing Award Rate Calculator Component
3: // Data from 2025 Plumbing and Fire Sprinklers Award rates (MA000036)
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: weekly: {
10: 'Worker Level 1(a)': 26.71,
11: 'Worker Level 1(b)': 27.6,
12: 'Worker Level 1(c)': 28.48,
13: 'Tradesperson Level 1': 30.45,
14: 'Tradesperson Level 1 (registered)': 31.33,
15: 'Advanced Tradesperson Level 2': 34.57,
16: 'Advanced Tradesperson Level 2 (registered)': 35.45,
17: },
18: casual: {
19: 'Worker Level 1(a)': 33.39,
20: 'Worker Level 1(b)': 34.5,
21: 'Worker Level 1(c)': 35.6,
22: 'Tradesperson Level 1': 38.06,
23: 'Tradesperson Level 1 (registered)': 39.16,
24: 'Advanced Tradesperson Level 2': 43.21,
25: 'Advanced Tradesperson Level 2 (registered)': 44.31,
26: },
27: }
28:
29: // Fixed award rules - non-editable, shows how the award works
30: const awardRules = [
31: {
32: id: 'ordinary',
33: name: 'Ordinary hours (Mon-Fri)',
34: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
35: startTime: '07:00',
36: endTime: '18:00',
37: penaltyMultiplier: { weekly: 1.0, casual: 1.0 },
38: },
39: {
40: id: 'saturday-first',
41: name: 'Saturday (first 2 hours)',
42: days: ['SAT'],
43: startTime: '00:00',
44: endTime: '02:00',
45: penaltyMultiplier: { weekly: 1.5, casual: 1.75 },
46: },
47: {
48: id: 'saturday-after',
49: name: 'Saturday (after 2 hours)',
50: days: ['SAT'],
51: startTime: '02:00',
52: endTime: '00:00',
53: penaltyMultiplier: { weekly: 2.0, casual: 2.25 },
54: },
55: {
56: id: 'sunday',
57: name: 'Sunday (all day)',
58: days: ['SUN'],
59: startTime: '00:00',
60: endTime: '00:00',
61: penaltyMultiplier: { weekly: 2.0, casual: 2.25 },
62: },
63: {
64: id: 'publicholiday',
65: name: 'Public holiday',
66: days: ['HOL'],
67: startTime: '00:00',
68: endTime: '00:00',
69: penaltyMultiplier: { weekly: 2.5, casual: 2.75 },
70: },
71: ]
72: ---
73:
74:
75:
76:
AWARD RATE ESTIMATOR
77:
See how RosterElf interprets the Plumbing Award
78:
79: This is an educational example showing how the Plumbing and Fire Sprinklers Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on
80: classification level, employment type, and shift times.
81:
82:
83:
84:
85:
86:
87:
88:
89:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
90:
official Fair Work pay guide
93: and consulting your Award obligations.
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
Employment type
104:
105:
106:
107:
108:
112: Weekly hire
113: Casual (includes 25% loading)
114:
115:
116:
117:
118:
119:
120:
Classification level
121:
122:
123:
124:
125:
129: Worker Level 1(a) – New entrant
130: Worker Level 1(b) – Plumber's assistant
131: Worker Level 1(c) – Experienced assistant
132: Tradesperson Level 1 – Qualified plumber
133: Tradesperson Level 1 (registered) – Registered plumber
134: Advanced Tradesperson Level 2 – Advanced tradesperson
135: Advanced Tradesperson Level 2 (registered) – Advanced registered
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
Base ordinary rate
147:
Mon-Fri, standard hours
148:
149:
150:
151: $
152: 26.71
153: /hr
154:
155:
156:
157:
158:
159:
160:
161:
162:
163: Plumbing Award penalty rates
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175: Example weekly cost (38 hours)
176:
177:
178:
179:
180:
181:
182:
183: Example total:
184: $1,014.98
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
Example only - not for payroll use
195:
This is a demonstration of how RosterElf calculates award-compliant rates.
196:
197:
198:
205: Important: Read all before using this calculator
206:
207:
208:
209:
210:
211:
212:
213:
The actual cost for your employees will depend on:
214:
215:
216: Their specific classification level and employment type
217: Actual hours worked and shift times
218: Any additional allowances, overtime, or enterprise agreement provisions
219: RDO accrual and arrangements
220: Current award rates (which change annually in July)
221:
222:
223:
For accurate payroll calculations, always:
224:
225:
226:
227: Verify current rates with the official Fair Work pay guide
233:
234: Confirm your employees' correct award coverage and classification
235: Use award interpretation software or consult a payroll professional
236: Review your specific enterprise agreement (if applicable)
237:
238:
239:
Do not rely on this example for actual wage payments.
240:
241:
242:
243:
244:
245:
246:
247:
248:
249:
Stop calculating penalty rates manually
250:
Let RosterElf handle award compliance automatically
251:
252:
253: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
254: the work for you.
255:
256:
257:
258:
259:
260:
261:
262:
263:
264: No credit card required
265:
266:
267:
268: Full access
269:
270:
271:
272: 24/7 support
273:
274:
275:
276:
277:
278:
279:
How RosterElf automates award calculations
280:
281:
282:
283:
284:
288:
Create pay templates
289:
290: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
291: each shift based on the employee's classification, shift timing, and employment type.
292:
293:
Award interpretation →
294:
295:
296:
297:
298:
302:
Define rate rules
303:
304: Configure when different penalty rates apply (Saturdays with 2-hour split, Sundays, public holidays). The system automatically detects which rate to use based on shift times and days.
305:
306:
Penalty rates guide →
307:
308:
309:
310:
311:
315:
Auto-apply to shifts
316:
317: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
318:
319:
Payroll integration →
320:
321:
322:
323:
324:
325:
📚 Learn more:
326:
333:
334:
335:
336:
337:
338:
563:
564:
````
## File: src/components/PopularIndustriesSection.astro
````astro
1: ---
2: import { ArrowRight, Hotel, UtensilsCrossed, Coffee, Stethoscope, Heart, ShoppingBag, ShoppingCart, Baby } from 'lucide-astro'
3:
4: interface Props {
5: background?: 'white' | 'primary'
6: title?: string
7: subtitle?: string
8: }
9:
10: const { background = 'primary', title = 'Popular industries we serve', subtitle = 'RosterElf is trusted by shift-based businesses across Australia' } = Astro.props
11:
12: // Curated list of popular industries for feature pages - mix of hubs and high-traffic sub-industries
13: const popularIndustries = [
14: { name: 'Hospitality', slug: 'hospitality', description: 'Hotels, pubs, bars & venues', icon: Hotel },
15: { name: 'Restaurants', slug: 'restaurants', description: 'Dining & food service', icon: UtensilsCrossed },
16: { name: 'Cafes', slug: 'cafe', description: 'Coffee shops & bakeries', icon: Coffee },
17: { name: 'Healthcare', slug: 'healthcare', description: 'Medical & allied health', icon: Stethoscope },
18: { name: 'Aged care', slug: 'aged-care', description: 'Residential & home care', icon: Heart },
19: { name: 'Retail', slug: 'retail', description: 'Shops & stores', icon: ShoppingBag },
20: { name: 'Supermarkets', slug: 'supermarkets', description: 'Grocery & convenience', icon: ShoppingCart },
21: { name: 'Childcare', slug: 'childcare', description: 'Early learning centres', icon: Baby },
22: ]
23:
24: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
25: ---
26:
27:
````
## File: src/components/PremiereGate.astro
````astro
1: ---
2: /**
3: * PremiereGate Component
4: * Prevents page from being accessed if premiere date hasn't passed
5: * Does server-side redirect to 404 for unpublished content (SEO-friendly)
6: * Preview mode: Add ?preview=true to URL to view unpublished content
7: *
8: * Usage at the TOP of your page:
9: * ---
10: * import PremiereGate from '../components/PremiereGate.astro'
11: * export const premiereDate = '2025-01-15'
12: * ---
13: *
14: *
15: *
16: *
17: *
18: */
19:
20: interface Props {
21: premiereDate?: string
22: showBadge?: boolean
23: }
24:
25: const { premiereDate, showBadge = false } = Astro.props
26:
27: // Determine if page should be visible based on date
28: let isVisible = true
29: let formattedDate = ''
30: let isPreviewMode = false
31:
32: if (premiereDate) {
33: const now = new Date()
34: const premiere = new Date(premiereDate)
35:
36: // Compare dates at midnight
37: now.setHours(0, 0, 0, 0)
38: premiere.setHours(0, 0, 0, 0)
39:
40: isVisible = premiere <= now
41:
42: // Check for preview mode (server-side)
43: isPreviewMode = Astro.url.searchParams.has('preview')
44:
45: formattedDate = premiere.toLocaleDateString('en-AU', {
46: year: 'numeric',
47: month: 'long',
48: day: 'numeric',
49: })
50:
51: // Server-side redirect to 404 if not visible and not in preview mode
52: if (!isVisible && !isPreviewMode) {
53: return Astro.redirect('/404', 404)
54: }
55: }
56: ---
57:
58: {/* Page is either visible or in preview mode */}
59: <>
60: {showBadge && premiereDate && isVisible && (
61:
62:
63:
64:
65:
Published {formattedDate}
66:
67: )}
68:
69:
70: >
````
## File: src/components/PremiereLink.astro
````astro
1: ---
2: /**
3: * PremiereLink Component
4: * Wrapper that shows/hides content based on premiere date
5: * Checks at build time - content not rendered if date hasn't passed
6: *
7: * Usage:
8: *
9: * New Feature
10: *
11: *
12: *
13: * ...
14: *
15: *
16: *
17: * Try New Feature
18: *
19: *
20: * Date format: YYYY-MM-DD
21: */
22:
23: interface Props {
24: premiereDate: string
25: }
26:
27: const { premiereDate } = Astro.props
28:
29: // Check if premiere date has passed
30: const now = new Date()
31: const premiere = new Date(premiereDate)
32:
33: now.setHours(0, 0, 0, 0)
34: premiere.setHours(0, 0, 0, 0)
35:
36: const isVisible = premiere <= now
37: ---
38:
39: {isVisible ? : null}
````
## File: src/components/PricingCards.astro
````astro
1: ---
2: /**
3: * PricingCards Component
4: *
5: * Reusable pricing cards that pull from centralized pricingData.ts
6: * Can be used on pricing page, comparison pages, etc.
7: *
8: * Props:
9: * - variant: 'full' (3 cards) | 'compact' (simplified for embedding)
10: * - showToggle: whether to show billing toggle (default: true)
11: * - background: 'white' | 'primary' (default: 'primary')
12: */
13:
14: import { Check, ChevronRight, Star, CalendarClock, Layers, Users } from 'lucide-astro'
15: import { pricingRates, plans, minimumMonthlySpend, annualDiscountPercent } from '../data/pricingData'
16: import TrialButton from './TrialButton.astro'
17:
18: interface Props {
19: variant?: 'full' | 'compact'
20: showToggle?: boolean
21: background?: 'white' | 'primary'
22: defaultBilling?: 'monthly' | 'annual'
23: currencySymbol?: string
24: currencyCode?: string
25: taxLabel?: string
26: taxPrefix?: string
27: rates?: typeof pricingRates
28: minimumSpend?: number
29: contactHref?: string
30: planFeatures?: Record
31: planTaglines?: Record
32: planDescriptions?: Record
33: planLinks?: Record
34: }
35:
36: const {
37: variant = 'full',
38: showToggle = true,
39: background = 'primary',
40: defaultBilling = 'monthly',
41: currencySymbol = '$',
42: currencyCode = 'AUD',
43: taxLabel = 'GST',
44: taxPrefix = '+',
45: rates = pricingRates,
46: minimumSpend = minimumMonthlySpend,
47: contactHref = '/why-rosterelf',
48: planFeatures,
49: planTaglines,
50: planDescriptions,
51: planLinks,
52: } = Astro.props
53:
54: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
55: const isCompact = variant === 'compact'
56:
57: // Icon mapping
58: const planIcons = {
59: core: CalendarClock,
60: fullSuite: Layers,
61: hr: Users,
62: }
63:
64: // Generate unique IDs for this instance (for multiple components on same page)
65: const instanceId = Math.random().toString(36).substring(7)
66: ---
67:
68:
69:
70: {
71: showToggle && (
72:
73:
74:
82: Annually
83:
84:
92: Monthly
93:
94:
95:
96: Save {annualDiscountPercent}% with annual billing
97:
98:
99: )
100: }
101:
102:
103: {
104: plans.map((plan) => {
105: const Icon = planIcons[plan.id]
106: const isPopular = plan.popular
107:
108: return (
109:
110: {isPopular && (
111:
112: MOST POPULAR
113:
114: )}
115:
116:
117:
118: {plan.name}
119:
120:
{planTaglines?.[plan.id] ?? plan.tagline}
121:
{planDescriptions?.[plan.id] ?? plan.description}
122:
123:
124:
125: {currencySymbol}
126:
127: {defaultBilling === 'monthly' ? rates[plan.id].monthly.toFixed(2) : rates[plan.id].annual.toFixed(2)}
128:
129:
130: {currencyCode}
131:
132:
per employee per month
133:
134: {taxPrefix} {taxLabel}, billed {defaultBilling === 'annual' ? 'annually' : 'monthly'}
135:
136:
137:
138:
139: {!isCompact && (
140: <>
141:
{plan.id === 'fullSuite' ? 'Everything in Core, plus:' : `${plan.name} features:`}
142:
143: {(planFeatures?.[plan.id] ?? plan.features).map((feature) => (
144:
145:
146: {feature}
147:
148: ))}
149:
150: >
151: )}
152:
153:
154: See all features
155:
156:
157: )
158: })
159: }
160:
161:
162:
163: {currencySymbol}{minimumSpend} minimum monthly spend applies to all plans. All prices exclude {taxLabel}.
164: Contact us for enterprise pricing (250+ employees).
165:
166:
167:
168:
169:
````
## File: src/components/RatingsSection.astro
````astro
1: ---
2: import { Star } from 'lucide-astro'
3: import { ratingsData, roundRating } from '../data/ratingsData'
4:
5: interface Props {
6: background?: 'primary' | 'white'
7: }
8:
9: const { background = 'primary' } = Astro.props
10: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
11:
12: // Rating data from centralized source with platform logos
13: const ratings = [
14: { name: ratingsData.platforms.xero.name, logo: '/images/logos/xero-app-store.svg', rating: ratingsData.platforms.xero.rating, width: 130, url: ratingsData.platforms.xero.url },
15: { name: ratingsData.platforms.google.name, logo: '/images/logos/google.png', rating: ratingsData.platforms.google.rating, width: 130, url: ratingsData.platforms.google.url },
16: { name: ratingsData.platforms.g2.name, logo: '/images/logos/g2-review.webp', rating: ratingsData.platforms.g2.rating, width: 150, url: ratingsData.platforms.g2.url },
17: { name: ratingsData.platforms.capterra.name, logo: '/images/logos/capterra.svg', rating: ratingsData.platforms.capterra.rating, width: 150, url: ratingsData.platforms.capterra.url },
18: ]
19:
20: // Aggregate rating for mobile display
21: const avgRating = ratingsData.aggregate.averageRating
22:
23: // Helper to generate star fill percentages
24: const getStarFills = (rating: number) => {
25: const fills = []
26: for (let i = 1; i <= 5; i++) {
27: if (rating >= i) {
28: fills.push(100) // Full star
29: } else if (rating > i - 1) {
30: fills.push(Math.round((rating - (i - 1)) * 100)) // Partial star
31: } else {
32: fills.push(0) // Empty star
33: }
34: }
35: return fills
36: }
37: ---
38:
39:
40:
41:
VERIFIED RATINGS
42:
43:
Trusted by 30,000+ workplaces
44:
45:
46:
47:
48: {Array.from({ length: 5 }).map(() => )}
49: {avgRating}+ average
50:
51:
52: Rated on Xero · Google · G2 · Capterra
53:
54:
55:
56:
57:
82:
83:
````
## File: src/components/ReadyToJoin.astro
````astro
1: ---
2: /**
3: * ReadyToJoin - CTA section encouraging users to start a free trial
4: *
5: * Usage:
6: *
7: *
8: */
9: import TrialButton from './TrialButton.astro'
10:
11: interface Props {
12: element: string
13: title?: string
14: description?: string
15: showPricing?: boolean
16: }
17:
18: const {
19: element,
20: title = 'Ready to join these successful businesses?',
21: description = 'Start your free trial today and see why thousands of Australian businesses trust RosterElf for their rostering and payroll.',
22: showPricing = true,
23: } = Astro.props
24: ---
25:
26:
27:
28:
{title}
29:
{description}
30:
43:
44:
````
## File: src/components/RelatedAwardsGuides.astro
````astro
1: ---
2: import { ArrowRight, Award } from 'lucide-astro'
3:
4: interface RelatedAward {
5: slug: string
6: name: string
7: }
8:
9: interface Props {
10: awards: RelatedAward[]
11: background?: 'white' | 'primary'
12: }
13:
14: const { awards, background = 'white' } = Astro.props
15:
16: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
17: ---
18:
19: {
20: awards.length > 0 && (
21:
22:
23:
24:
RELATED GUIDES
25:
Related award rate guides
26:
Explore other Australian Modern Award pay rate guides
27:
28:
29:
45:
46:
47: View all award guides
48:
49:
50:
51:
52: )
53: }
````
## File: src/components/RelatedAwardsSection.astro
````astro
1: ---
2: import { ArrowRight, Award } from 'lucide-astro'
3: import { getIndustryAwards, type AwardInfo } from '../data/awardMappings'
4:
5: interface Props {
6: industrySlug: string
7: industryName: string
8: background?: 'white' | 'primary'
9: }
10:
11: const { industrySlug, industryName, background = 'white' } = Astro.props
12:
13: const relatedAwards = getIndustryAwards(industrySlug)
14: const hasAwards = relatedAwards.length > 0
15:
16: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
17: ---
18:
19: {
20: hasAwards && (
21:
22:
23:
24:
AWARD RATES
25:
Relevant awards for {industryName.toLowerCase()}
26:
Stay compliant with Australian Modern Award pay rates
27:
28:
29:
45:
46:
47: View all award guides
48:
49:
50:
51:
52: )
53: }
````
## File: src/components/RelatedBlogsSection.astro
````astro
1: ---
2: /**
3: * RelatedBlogsSection - Display related blog posts on feature/industry/guide pages
4: *
5: * Usage (manual posts):
6: *
11: *
12: * Usage (auto-fetch by category):
13: *
17: *
18: * Note: Auto-fetched posts respect premiere dates (only published content shown)
19: */
20: import { ArrowRight, Clock } from 'lucide-astro'
21:
22: interface BlogPost {
23: title: string
24: excerpt: string
25: href: string
26: image: string
27: category?: string
28: readTime?: string
29: }
30:
31: interface Props {
32: title?: string
33: subtitle?: string
34: posts?: BlogPost[]
35: category?: string // Auto-fetch from this category if no posts provided
36: background?: 'white' | 'primary' | 'gray'
37: showViewAll?: boolean
38: viewAllHref?: string
39: viewAllText?: string
40: }
41:
42: const {
43: title = 'Related insights',
44: subtitle = 'Learn more about this topic from our blog.',
45: posts: manualPosts,
46: category,
47: background = 'primary',
48: showViewAll = true,
49: viewAllHref = '/blog',
50: viewAllText = 'View all articles',
51: } = Astro.props
52:
53: // Check if a post is published (premiere date has passed or doesn't exist)
54: function isPublished(premiereDate?: string): boolean {
55: if (!premiereDate) return true
56: const now = new Date()
57: const premiere = new Date(premiereDate)
58: now.setHours(0, 0, 0, 0)
59: premiere.setHours(0, 0, 0, 0)
60: return premiere <= now
61: }
62:
63: // Auto-fetch posts if category is provided and no manual posts given
64: let posts: BlogPost[] = manualPosts || []
65:
66: if (!manualPosts && category) {
67: // Fetch all blog posts and extract metadata
68: const blogModules = import.meta.glob('../pages/blog/*.astro', { eager: true }) as Record
69:
70: // Process blogs: filter published, filter by category, sort by date
71: const filteredBlogs = Object.entries(blogModules)
72: .filter(([_, mod]) => mod.articleMeta && isPublished(mod.premiereDate))
73: .filter(([_, mod]) => mod.articleMeta.category === category)
74: .map(([path, mod]) => ({
75: title: mod.articleMeta.title,
76: excerpt: mod.articleMeta.excerpt,
77: href: `/blog/${path.split('/').pop()?.replace('.astro', '')}`,
78: image: mod.articleMeta.image,
79: category: mod.articleMeta.category,
80: readTime: mod.articleMeta.readTime,
81: dateISO: mod.articleMeta.dateISO || '',
82: }))
83: .sort((a, b) => (b.dateISO || '').localeCompare(a.dateISO || ''))
84: .slice(0, 3)
85:
86: posts = filteredBlogs
87: }
88:
89: const bgClass = {
90: white: 'bg-white',
91: primary: 'bg-primary-100',
92: gray: 'bg-gray-50',
93: }[background]
94:
95: // Card background should contrast with section background
96: const cardBg = background === 'white' ? 'bg-gray-50' : 'bg-white'
97: ---
98:
99: {
100: posts.length > 0 && (
101:
102:
103:
104:
105:
{title}
106: {subtitle &&
{subtitle}
}
107:
108: {showViewAll && (
109:
110: {viewAllText}
111:
112: )}
113:
114:
115:
135:
136:
137: )
138: }
````
## File: src/components/RelatedFeaturesKB.astro
````astro
1: ---
2: /**
3: * RelatedFeaturesKB - Related features component specifically for Knowledge Base articles
4: * Features:
5: * - Contained width matching the page layout
6: * - White box container matching Related Articles style
7: * - Feature cards with hover effects
8: */
9: import { ArrowRight } from 'lucide-astro'
10: import { getRelatedFeatures, getFeatureUrl, type FeatureInfo } from '../data/featureMappings'
11:
12: interface Props {
13: currentFeature: string
14: }
15:
16: const { currentFeature } = Astro.props
17:
18: const relatedFeatures = getRelatedFeatures(currentFeature)
19: ---
20:
21: {
22: relatedFeatures.length > 0 && (
23:
24:
25:
26: {/* Header section */}
27:
28:
Explore related features
29:
30:
31: {/* Feature cards */}
32:
33:
58:
59: {/* View all features link */}
60:
66:
67:
68:
69:
70: )
71: }
````
## File: src/components/RelatedFeaturesSection.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import { getRelatedFeatures, getFeatureUrl, type FeatureInfo } from '../data/featureMappings'
4:
5: interface Props {
6: currentFeature: string
7: background?: 'white' | 'primary'
8: }
9:
10: const { currentFeature, background = 'primary' } = Astro.props
11:
12: const relatedFeatures = getRelatedFeatures(currentFeature)
13:
14: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
15: ---
16:
17: {
18: relatedFeatures.length > 0 && (
19:
20:
21:
22:
RELATED FEATURES
23:
Explore related features
24:
Discover other RosterElf features that work great together
25:
26:
27:
41:
42:
43: View all features
44:
45:
46:
47:
48: )
49: }
````
## File: src/components/RelatedGuidesSection.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
4: import { readFile } from 'node:fs/promises'
5: import { join } from 'node:path'
6:
7: interface Guide {
8: href: string
9: title: string
10: description: string
11: icon: AstroComponentFactory
12: linkText?: string
13: }
14:
15: interface Props {
16: title?: string
17: subtitle?: string
18: guides: Guide[]
19: background?: 'white' | 'primary'
20: maxWidth?: string
21: }
22:
23: /**
24: * Check if a blog post has a future premiere date
25: */
26: async function hasFuturePremiereDate(blogSlug: string): Promise {
27: try {
28: const filePath = join(process.cwd(), 'src/pages/blog', `${blogSlug}.astro`)
29: const content = await readFile(filePath, 'utf-8')
30:
31: // Extract premiere date from content
32: const premiereDateMatch = content.match(/premiereDate\s*=\s*['"]([^'"]+)['"]/)
33: if (!premiereDateMatch) return false
34:
35: const premiereDate = new Date(premiereDateMatch[1])
36: const now = new Date()
37:
38: // Compare dates at midnight
39: now.setHours(0, 0, 0, 0)
40: premiereDate.setHours(0, 0, 0, 0)
41:
42: return premiereDate > now
43: } catch {
44: // If file doesn't exist or can't be read, assume it's not a blog post or is published
45: return false
46: }
47: }
48:
49: /**
50: * Filter out guides with future premiere dates (blog posts only)
51: */
52: async function filterPublishedGuides(guides: Guide[]): Promise {
53: const results = await Promise.all(
54: guides.map(async (guide) => {
55: // Only check blog posts (href starts with /blog/)
56: if (!guide.href.startsWith('/blog/')) return { guide, isPublished: true }
57:
58: // Extract slug from href
59: const slug = guide.href.replace('/blog/', '')
60: const isFuture = await hasFuturePremiereDate(slug)
61:
62: return { guide, isPublished: !isFuture }
63: })
64: )
65:
66: return results.filter((r) => r.isPublished).map((r) => r.guide)
67: }
68:
69: const { title = 'Related guides', subtitle = 'More resources to help you.', guides, background = 'primary', maxWidth = 'max-w-7xl' } = Astro.props
70:
71: // Filter out unpublished blog posts
72: const publishedGuides = await filterPublishedGuides(guides)
73:
74: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
75: ---
76:
77: {
78: publishedGuides.length > 0 && (
79:
80:
81:
82:
{title}
83:
{subtitle}
84:
85:
105:
106:
107: )
108: }
````
## File: src/components/RelatedIndustriesSection.astro
````astro
1: ---
2: import { ArrowRight } from 'lucide-astro'
3: import { getRelatedIndustries, type IndustryInfo } from '../data/industryMappings'
4:
5: interface Props {
6: currentIndustry: string
7: background?: 'white' | 'primary'
8: }
9:
10: const { currentIndustry, background = 'primary' } = Astro.props
11:
12: const relatedIndustries = getRelatedIndustries(currentIndustry)
13:
14: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
15: ---
16:
17: {
18: relatedIndustries.length > 0 && (
19:
20:
21:
22:
RELATED INDUSTRIES
23:
Explore similar industries
24:
See how RosterElf helps other businesses like yours
25:
26:
27:
38:
39:
40: View all industries
41:
42:
43:
44:
45: )
46: }
````
## File: src/components/RelatedProductUpdates.astro
````astro
1: ---
2: import { ArrowRight, Calendar, Sparkles } from 'lucide-astro'
3:
4: interface ProductUpdate {
5: title: string
6: excerpt: string
7: href: string
8: date: string
9: tag: string
10: }
11:
12: interface Props {
13: updates: ProductUpdate[]
14: background?: 'white' | 'primary'
15: maxWidth?: string
16: chip?: string
17: title?: string
18: description?: string
19: }
20:
21: const {
22: updates,
23: background = 'white',
24: maxWidth = 'max-w-7xl',
25: chip = 'MORE UPDATES',
26: title = 'Other recent product updates',
27: description = "Discover the latest features and improvements we've shipped",
28: } = Astro.props
29:
30: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
31: ---
32:
33: {
34: updates.length > 0 && (
35:
75: )
76: }
````
## File: src/components/RelatedResourcesGrid.astro
````astro
1: ---
2: /**
3: * RelatedResourcesGrid - Tabbed Related Resources Component
4: *
5: * Displays related guides, insights, and templates for free tool pages.
6: * Uses tabbed interface for switching between resource types.
7: *
8: * Props:
9: * - toolId: string - The tool slug to fetch related resources for
10: * - background: "white" | "primary" (default: "white")
11: */
12:
13: import { getToolRelatedResources, type RelatedResource } from '../data/freeToolRelatedResources'
14: import { BookOpen, Lightbulb, FileText, ArrowRight } from 'lucide-astro'
15:
16: interface Props {
17: toolId: string
18: background?: 'white' | 'primary'
19: }
20:
21: const { toolId, background = 'white' } = Astro.props
22:
23: // Get related resources for this tool
24: const resources = getToolRelatedResources(toolId)
25:
26: // Generate unique ID for this instance
27: const instanceId = Math.random().toString(36).substring(7)
28:
29: // Tab configuration
30: const tabs = [
31: { id: 'guides', label: 'Guides', icon: BookOpen, items: resources?.guides || [] },
32: { id: 'insights', label: 'Insights', icon: Lightbulb, items: resources?.insights || [] },
33: { id: 'templates', label: 'Templates', icon: FileText, items: resources?.templates || [] },
34: ]
35:
36: // Only render if we have resources
37: const hasResources = tabs.some((tab) => tab.items.length > 0)
38: ---
39:
40: {
41: hasResources && (
42:
43:
44: {/* Header */}
45:
46:
Related resources
47:
Explore guides, insights, and templates to help you get more from this tool.
48:
49:
50: {/* Tabs */}
51:
52: {tabs.map((tab, index) => {
53: const IconComponent = tab.icon
54: const hasItems = tab.items.length > 0
55: return (
56:
73:
74: {tab.label}
75: ({tab.items.length})
76:
77: )
78: })}
79:
80:
81: {/* Content Panels */}
82:
83: {tabs.map((tab, index) => (
84:
85: {tab.items.length > 0 ? (
86:
126: ) : (
127:
No {tab.label.toLowerCase()} available for this tool.
128: )}
129:
130: ))}
131:
132:
133:
134: )
135: }
136:
137:
````
## File: src/components/RelatedSupportArticles.astro
````astro
1: ---
2: import { ArrowRight, BookOpen } from 'lucide-astro'
3:
4: interface SupportArticle {
5: title: string
6: excerpt: string
7: href: string
8: category?: string
9: }
10:
11: interface Props {
12: articles: SupportArticle[]
13: background?: 'white' | 'primary'
14: maxWidth?: string
15: }
16:
17: const { articles, background = 'white', maxWidth = 'max-w-7xl' } = Astro.props
18:
19: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
20: ---
21:
22: {
23: articles.length > 0 && (
24:
58: )
59: }
````
## File: src/components/ResponsiveLink.astro
````astro
1: ---
2: interface Props {
3: href: string
4: mobileText: string
5: desktopText: string
6: target?: string
7: rel?: string
8: class?: string
9: }
10:
11: const { href, mobileText, desktopText, target, rel, class: className } = Astro.props
12: ---
13:
14:
15: {mobileText}
16: {desktopText}
17:
````
## File: src/components/RestaurantAwardCalculator.astro
````astro
1: ---
2: // Restaurant Award Rate Calculator Component
3: // Data from 2025 Restaurant Industry Award 2020 [MA000119] rates
4:
5: import { AlertTriangle, FileText, Clock, CheckCircle, ChevronDown, User, Calculator } from 'lucide-astro'
6: import TrialButton from './TrialButton.astro'
7:
8: const ratesData = {
9: permanent: {
10: Introductory: 24.28,
11: 'Level 1': 24.95,
12: 'Level 2': 25.85,
13: 'Level 3': 26.7,
14: 'Level 4': 28.12,
15: 'Level 5': 29.88,
16: 'Level 6': 30.68,
17: },
18: casual: {
19: Introductory: 30.35,
20: 'Level 1': 31.19,
21: 'Level 2': 32.31,
22: 'Level 3': 33.38,
23: 'Level 4': 35.15,
24: 'Level 5': 37.35,
25: 'Level 6': 38.35,
26: },
27: }
28:
29: // Fixed award rules - Restaurant Award has NO evening or late night penalties
30: const awardRules = [
31: {
32: id: 'ordinary',
33: name: 'Ordinary hours (Mon-Fri)',
34: days: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
35: startTime: '07:00',
36: endTime: '19:00',
37: penaltyMultiplier: { permanent: 1.0, casual: 1.0 },
38: },
39: {
40: id: 'saturday',
41: name: 'Saturday (all day)',
42: days: ['SAT'],
43: startTime: '00:00',
44: endTime: '00:00',
45: penaltyMultiplier: { permanent: 1.5, casual: 1.75 },
46: },
47: {
48: id: 'sunday',
49: name: 'Sunday (all day)',
50: days: ['SUN'],
51: startTime: '00:00',
52: endTime: '00:00',
53: penaltyMultiplier: { permanent: 1.75, casual: 2.0 },
54: },
55: {
56: id: 'publicholiday',
57: name: 'Public holiday',
58: days: ['HOL'],
59: startTime: '00:00',
60: endTime: '00:00',
61: penaltyMultiplier: { permanent: 2.5, casual: 2.75 },
62: },
63: ]
64: ---
65:
66:
67:
68:
AWARD RATE ESTIMATOR
69:
See how RosterElf interprets the Restaurant Award
70:
71: This is an educational example showing how the Restaurant Award penalty rates work. It demonstrates how RosterElf automatically calculates correct pay rates based on classification level,
72: employment type, and shift times.
73:
74:
75:
76:
77:
78:
79:
80:
81:
Important: This is an estimator for demonstration purposes only. Do not use these calculations for actual payroll without verifying against the
82:
official Fair Work MA000119 summary
88: and consulting your Award obligations.
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
Employment type
99:
100:
101:
102:
103:
107: Full-time / Part-time
108: Casual (includes 25% loading)
109:
110:
111:
112:
113:
114:
115:
Classification level
116:
117:
118:
119:
120:
124: Level 1 – Wait staff, Barista, Dishwasher, Host/Hostess
125: Introductory
126: Level 2 – Cook, Experienced Wait Staff, Shift Supervisor
127: Level 3 – Skilled Cook, Senior Wait Staff, Restaurant Supervisor
128: Level 4 – Trade-Qualified Chef, Head Waiter
129: Level 5 – Senior Chef, Sous Chef, Restaurant Manager
130: Level 6 – Head Chef, Executive Chef, Senior Manager
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
Base ordinary rate
142:
Mon-Fri, standard hours
143:
144:
145:
146: $
147: 24.95
148: /hr
149:
150:
151:
152:
153:
154:
155:
156:
157:
158: Restaurant Award penalty rates
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170: Example weekly cost (38 hours)
171:
172:
173:
174:
175:
176:
177:
178: Example total:
179: $1,025.29
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
Example only - not for payroll use
190:
This is a demonstration of how RosterElf calculates award-compliant rates.
191:
192:
193:
200: Important: Read all before using this calculator
201:
202:
203:
204:
205:
206:
207:
208:
The actual cost for your employees will depend on:
209:
210:
211: Their specific classification level and employment type
212: Actual hours worked and shift times
213: Any additional allowances, overtime, or enterprise agreement provisions
214: Current award rates (which change annually in July)
215:
216:
217:
For accurate payroll calculations, always:
218:
219:
220:
221: Verify current rates with the official Fair Work MA000119 summary
227:
228: Confirm your employees' correct award coverage and classification
229: Use award interpretation software or consult a payroll professional
230: Review your specific enterprise agreement (if applicable)
231:
232:
233:
Do not rely on this example for actual wage payments.
234:
235:
236:
237:
238:
239:
240:
241:
242:
243:
Stop calculating penalty rates manually
244:
Let RosterElf handle award compliance automatically
245:
246:
247: Manual award calculations are time-consuming and error-prone. One mistake can lead to underpayments, compliance issues, and Fair Work penalties. RosterElf's award interpretation engine does
248: the work for you.
249:
250:
251:
252:
253:
254:
255:
256:
257:
258: No credit card required
259:
260:
261:
262: Full access
263:
264:
265:
266: 24/7 support
267:
268:
269:
270:
271:
272:
273:
How RosterElf automates award calculations
274:
275:
276:
277:
278:
282:
Create pay templates
283:
284: Create pay templates for each classification level by adding award-compliant base rates and penalty multipliers. Once configured, RosterElf automatically applies the correct template to
285: each shift based on the employee's classification, shift timing, and employment type.
286:
287:
Award interpretation →
288:
289:
290:
291:
292:
296:
Define rate rules
297:
298: Configure when different penalty rates apply (weekends, public holidays). The system automatically detects which rate to use based on shift times and days.
299:
300:
Penalty rates guide →
301:
302:
303:
304:
305:
309:
Auto-apply to shifts
310:
311: Every rostered shift automatically calculates the correct pay rate based on the employee's classification, employment type, and shift timing. No manual work required.
312:
313:
Payroll integration →
314:
315:
316:
317:
318:
319:
Learn more:
320:
327:
328:
329:
330:
331:
332:
557:
558:
````
## File: src/components/ReviewCarousel.astro
````astro
1: ---
2: import { Star } from 'lucide-astro'
3:
4: interface Review {
5: platform: 'google' | 'g2' | 'capterra' | 'xero'
6: platformLogo: string
7: rating: number
8: text: string
9: author: string
10: authorImage?: string
11: authorInitials?: string
12: date: string
13: }
14:
15: interface Props {
16: chip?: string
17: title?: string
18: description?: string
19: ctaText?: string
20: ctaUrl?: string
21: reviews?: Review[]
22: background?: 'primary' | 'white'
23: }
24:
25: const defaultReviews: Review[] = [
26: {
27: platform: 'google',
28: platformLogo: '/images/logos/google.svg',
29: rating: 5,
30: text: "We are extremely happy with Roster Elf, it's a great improvement on the previous system we were using. Sean and all the staff have been fantastic, really know their stuff and make using RosterElf a pleasure.",
31: author: 'Andrew Nathan',
32: authorInitials: 'AN',
33: date: 'November 13, 2025',
34: },
35: {
36: platform: 'google',
37: platformLogo: '/images/logos/google.svg',
38: rating: 5,
39: text: 'Sean was an amazing help, very patient and thorough.',
40: author: 'Monica Van De Laak',
41: authorInitials: 'MV',
42: date: 'October 16, 2025',
43: },
44: {
45: platform: 'g2',
46: platformLogo: '/images/logos/g2-logo.webp',
47: rating: 5,
48: text: 'I can see my roster, enter availability, put leave in advance and clock on and off easily.',
49: author: 'Mary C.',
50: authorInitials: 'MC',
51: date: 'October 13, 2025',
52: },
53: {
54: platform: 'google',
55: platformLogo: '/images/logos/google.svg',
56: rating: 5,
57: text: 'Really detailed and helpful. First time user of a rostering software and I was provided with clear and easy to follow instructions with the promise of being available for any further questions or help that I might need in the future. Would definitely recommend',
58: author: 'Partap Singh Thind',
59: authorInitials: 'PT',
60: date: 'December 05, 2025',
61: },
62: {
63: platform: 'google',
64: platformLogo: '/images/logos/google.svg',
65: rating: 5,
66: text: 'Very easy to use and navigate. The support team is always quick to respond and helpful with any queries.',
67: author: 'Sarah Johnson',
68: authorInitials: 'SJ',
69: date: 'September 28, 2025',
70: },
71: {
72: platform: 'g2',
73: platformLogo: '/images/logos/g2-logo.webp',
74: rating: 5,
75: text: 'Great tool for managing staff rosters and timesheets. Has made our scheduling much more efficient.',
76: author: 'David L.',
77: authorInitials: 'DL',
78: date: 'August 15, 2025',
79: },
80: {
81: platform: 'google',
82: platformLogo: '/images/logos/google.svg',
83: rating: 5,
84: text: 'Excellent rostering software! The drag and drop feature makes creating weekly schedules a breeze. Our team loves the mobile app for checking their shifts.',
85: author: 'James Mitchell',
86: authorInitials: 'JM',
87: date: 'November 28, 2025',
88: },
89: {
90: platform: 'capterra',
91: platformLogo: '/images/logos/capterra.svg',
92: rating: 5,
93: text: 'RosterElf has transformed how we manage our hospitality staff. The award interpretation feature alone saves us hours every pay period.',
94: author: 'Emma Richardson',
95: authorInitials: 'ER',
96: date: 'October 22, 2025',
97: },
98: {
99: platform: 'google',
100: platformLogo: '/images/logos/google.svg',
101: rating: 5,
102: text: 'Finally found a rostering solution that understands Australian workplace requirements. The Fair Work compliance features give us peace of mind.',
103: author: 'Michael Chen',
104: authorInitials: 'MC',
105: date: 'September 15, 2025',
106: },
107: {
108: platform: 'g2',
109: platformLogo: '/images/logos/g2-logo.webp',
110: rating: 5,
111: text: 'The integration with Xero is smooth. Timesheets flow directly into payroll without any manual data entry. Highly recommend for any small business.',
112: author: 'Rachel K.',
113: authorInitials: 'RK',
114: date: 'August 30, 2025',
115: },
116: ]
117:
118: const {
119: chip = 'WHAT OTHERS SAY',
120: title = 'Reviews from our customers',
121: description = 'Read what our clients have to say about us',
122: ctaText = 'See all reviews',
123: ctaUrl = '/why-rosterelf/online-reviews',
124: reviews = defaultReviews,
125: background = 'primary',
126: } = Astro.props
127:
128: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
129: ---
130:
131:
132:
133:
134:
135:
{chip}
136:
{title}
137: {description &&
{description}
}
138:
139:
140: {[1, 2, 3, 4, 5].map(() => )}
141:
142:
4.8
143:
average rating
144:
145:
146:
147:
148:
149: {
150: reviews.slice(0, 3).map((review) => (
151:
152:
153:
154: {review.authorImage ? (
155:
156: ) : (
157:
{review.authorInitials}
158: )}
159:
160:
{review.author}
161:
{review.date}
162:
163:
164:
170:
171:
172: {Array.from({ length: 5 }, (_, i) => (
173:
174: ))}
175:
176:
"{review.text}"
177:
178: ))
179: }
180:
181:
182:
183:
218:
219:
220: {
221: ctaUrl && (
222:
226: {ctaText}
227:
228: )
229: }
230:
231:
232:
233:
311:
312:
````
## File: src/components/ReviewMethodologyCallout.astro
````astro
1: ---
2: import { FileText, ArrowRight } from 'lucide-astro'
3:
4: interface Props {
5: inline?: boolean
6: background?: 'white' | 'primary'
7: }
8:
9: const { inline = false, background = 'white' } = Astro.props
10: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
11: ---
12:
13: {
14: inline ? (
15:
27: ) : (
28:
42: )
43: }
````
## File: src/components/ReviewsWithComments.astro
````astro
1: ---
2: import { Star, HelpCircle } from 'lucide-astro'
3:
4: interface Props {
5: review: {
6: initials: string
7: name: string
8: date: string
9: quote: string
10: logo?: 'xero' | 'g2' | 'capterra'
11: }
12: content: {
13: chip: string
14: heading: string
15: description: string
16: linkText?: string
17: linkHref?: string
18: }
19: reversed?: boolean
20: }
21:
22: const { review, content, reversed = false } = Astro.props
23: ---
24:
25: *:first-child]:order-2' : ''}`}>
26:
27:
28:
29:
30:
{review.initials}
31:
32:
{review.name}
33:
{review.date}
34:
35:
36: {review.logo === 'xero' &&
}
37: {review.logo === 'g2' &&
}
38: {review.logo === 'capterra' &&
}
39:
40:
41:
42:
43:
44:
45:
46:
47:
"{review.quote}"
48:
49:
50:
51:
52:
{content.chip}
53:
{content.heading}
54:
{content.description}
55: {
56: content.linkHref && content.linkText && (
57:
58:
59: {content.linkText}
60:
61: )
62: }
63:
64:
````
## File: src/components/ROICalculator.astro
````astro
1: ---
2: import TrialButton from './TrialButton.astro'
3:
4: interface Props {
5: chip?: string
6: title?: string
7: description?: string
8: background?: 'primary' | 'white'
9: currencySymbol?: string
10: currencyLocale?: string
11: defaultWage?: number
12: }
13:
14: const {
15: chip = 'ROI CALCULATOR',
16: title = 'Calculate your rostering savings',
17: description = 'See how much time and money your business could save with RosterElf rostering software.',
18: background = 'white',
19: currencySymbol = '$',
20: currencyLocale = 'en-AU',
21: defaultWage = 45,
22: } = Astro.props
23:
24: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
25: ---
26:
27:
28:
29:
30:
{chip}
31:
{title}
32:
{description}
33:
34:
35:
36:
37:
38:
Your current rostering
39:
40:
41: Number of employees
42:
50:
51:
52:
53: Hours spent rostering per week
54:
62:
63:
64:
65: Manager hourly rate ({currencySymbol})
66:
74:
75:
76:
Based on 70% time reduction with automated rostering
77:
78:
79:
80:
81:
Your potential savings
82:
83:
84:
85: Time saved per week
86: 3.5 hours
87:
88:
89:
90: Annual time saved
91: 182 hours
92:
93:
94:
95: Annual cost savings
96: {currencySymbol}8,190
97:
98:
99:
100:
101:
102:
No credit card required
103:
104:
105:
106:
107:
108:
109:
````
## File: src/components/SimpleCTA.astro
````astro
1: ---
2: import TrialButton from './TrialButton.astro'
3:
4: interface Props {
5: title: string
6: description: string
7: }
8:
9: const { title, description } = Astro.props
10: ---
11:
12:
13:
14:
15:
{title}
16:
{description}
17:
18:
19:
20:
````
## File: src/components/SoftwareFeatureSchema.astro
````astro
1: ---
2: /**
3: * SoftwareFeatureSchema - Generates schema.org SoftwareApplication markup for feature pages
4: *
5: * This component adds structured data to help search engines understand
6: * that this page describes a specific feature of RosterElf software.
7: */
8: import { ratingsData } from '../data/ratingsData'
9:
10: interface Props {
11: name: string // Feature name e.g., "Rostering Software", "Time & Attendance"
12: description: string // Page meta description
13: url: string // Canonical URL path e.g., "/features/rostering-software"
14: image?: string // Hero image URL
15: datePublished?: string // ISO date when feature was first published
16: dateModified?: string // ISO date when feature was last updated
17: applicationSubCategory?: string // Optional subcategory override, defaults to "Workforce Management"
18: featureList?: string[] // Optional feature list override, defaults to core features
19: price?: string // Override price, defaults to '4.00' (AUD)
20: priceCurrency?: string // Override currency, defaults to 'AUD'
21: eligibleRegion?: string // Override region, defaults to 'Australia'
22: }
23:
24: const { name, description, url, image, datePublished, dateModified, applicationSubCategory, featureList, price: offerPrice = '4.00', priceCurrency = 'AUD', eligibleRegion = 'Australia' } = Astro.props
25:
26: const softwareSchema = {
27: '@context': 'https://schema.org',
28: '@type': 'SoftwareApplication',
29: name: `RosterElf ${name}`,
30: description: description,
31: url: `https://www.rosterelf.com${url}`,
32: ...(image && { image: `https://www.rosterelf.com${image}` }),
33: ...(datePublished && { datePublished }),
34: ...(dateModified && { dateModified }),
35: applicationCategory: 'BusinessApplication',
36: applicationSubCategory: applicationSubCategory || 'Workforce Management',
37: operatingSystem: 'Web, iOS, Android',
38: provider: {
39: '@type': 'Organization',
40: name: 'RosterElf',
41: url: 'https://www.rosterelf.com',
42: logo: 'https://www.rosterelf.com/images/logos/rosterelf-logo.svg',
43: },
44: aggregateRating: {
45: '@type': 'AggregateRating',
46: ratingValue: String(ratingsData.aggregate.averageRating),
47: bestRating: '5',
48: worstRating: '1',
49: ratingCount: String(ratingsData.aggregate.totalReviews),
50: },
51: offers: {
52: '@type': 'Offer',
53: price: offerPrice,
54: priceCurrency: priceCurrency,
55: priceValidUntil: '2026-12-31',
56: eligibleRegion: {
57: '@type': 'Country',
58: name: eligibleRegion,
59: },
60: availability: 'https://schema.org/OnlineOnly',
61: },
62: featureList: featureList || ['Drag-and-drop rostering', 'Auto-scheduling', 'Time and attendance tracking', 'Award interpretation', 'Payroll integration', 'Mobile app access'],
63: }
64: ---
65:
66:
````
## File: src/components/SupportImage.astro
````astro
1: ---
2: /**
3: * SupportImage - Optimised image component for support articles
4: *
5: * Uses Astro's built-in Image component for automatic optimization:
6: * - Generates WebP/AVIF formats automatically
7: * - Lazy loading by default
8: * - Prevents layout shift with width/height
9: *
10: * USAGE FOR NEW ARTICLES (recommended):
11: * Import image from src/assets/support/ and pass as src:
12: *
13: * ```astro
14: * ---
15: * import SupportImage from '../../../../components/SupportImage.astro'
16: * import screenshot from '../../../../assets/support/article-name/screenshot.png'
17: * ---
18: *
19: * ```
20: *
21: * USAGE FOR EXISTING ARTICLES (legacy):
22: * Pass URL string for images in public folder (no optimization):
23: *
24: * ```astro
25: *
26: * ```
27: *
28: * For above-the-fold images (first image visible on page load):
29: * ```astro
30: *
31: * ```
32: */
33:
34: import { Image } from 'astro:assets'
35: import type { ImageMetadata } from 'astro'
36:
37: interface Props {
38: src: ImageMetadata | string // Imported image or URL string
39: alt: string
40: width?: number // Only needed for URL strings
41: height?: number // Only needed for URL strings
42: class?: string
43: caption?: string
44: priority?: boolean // Set true for above-the-fold images
45: }
46:
47: const { src, alt, width = 1200, height = 675, class: className = '', caption, priority = false } = Astro.props
48:
49: // Check if src is an imported image (ImageMetadata) or a URL string
50: const isImportedImage = typeof src !== 'string'
51: ---
52:
53:
54: {
55: isImportedImage ? (
56:
57: ) : (
58:
68: )
69: }
70: {caption && {caption} }
71:
````
## File: src/components/SupportSection.astro
````astro
1: ---
2: import { Star, Check } from 'lucide-astro'
3:
4: interface Props {
5: chip?: string
6: title?: string
7: description?: string
8: bullets?: string[]
9: showRating?: boolean
10: ratingText?: string
11: ratingCaption?: string
12: image?: string
13: imageAlt?: string
14: demoUrl?: string
15: demoText?: string
16: chatText?: string
17: background?: 'primary' | 'white'
18: }
19:
20: const {
21: chip = 'SUPPORT',
22: title = 'Real support from people who understand your business',
23: description = "At RosterElf, support isn't a ticket system — it's part of the product. Our Australian-based team helps you set up correctly, understand award rules, and stay compliant as your business changes. No scripts. No offshore handoffs. Just real help when you need it.",
24: bullets = ['Guided setup and onboarding', 'Award and payroll questions answered', 'Ongoing help as your team grows'],
25: showRating = true,
26: ratingText = '5.0 average rating',
27: ratingCaption = 'Rated 5.0 by Australian businesses',
28: image = '/images/support-home.webp',
29: imageAlt = 'RosterElf support team on call',
30: demoUrl = 'https://calendly.com/d/dpy-jkv-sd6/rosterelf-demo',
31: demoText = 'Book a demo',
32: chatText = 'Talk to support',
33: background = 'primary',
34: } = Astro.props
35:
36: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
37: ---
38:
39:
40:
41:
42:
43:
44:
{chip}
45:
{title}
46:
{description}
47: {
48: bullets && bullets.length > 0 && (
49:
50: {bullets.map((bullet) => (
51:
52:
53:
54:
55: {bullet}
56:
57: ))}
58:
59: )
60: }
61:
75:
76:
77:
78:
79: {
80: showRating && (
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
{ratingText}
93:
94:
95:
{ratingCaption}
96:
97: )
98: }
99:
100:
101:
102:
103:
````
## File: src/components/Table.astro
````astro
1: ---
2: /**
3: * Responsive Table Component
4: *
5: * Automatically handles mobile-responsive tables with data-label attributes.
6: * Works with global.css mobile table styles that convert tables to card layout.
7: *
8: * Usage:
9: *
16: */
17:
18: interface Props {
19: headers: string[]
20: rows: (string | number)[][]
21: caption?: string
22: striped?: boolean
23: compact?: boolean
24: }
25:
26: const { headers, rows, caption, striped = true, compact = false } = Astro.props
27:
28: const cellPadding = compact ? 'px-3 py-2' : 'px-4 py-3'
29: const headerPadding = compact ? 'px-3 py-3' : 'px-4 py-4'
30: ---
31:
32:
33:
34: {caption && {caption} }
35:
36:
37: {
38: headers.map((header, index) => (
39: {header}
40: ))
41: }
42:
43:
44:
45: {
46: rows.map((row, rowIndex) => (
47:
48: {row.map((cell, cellIndex) => (
49:
53: {cell}
54:
55: ))}
56:
57: ))
58: }
59:
60:
61:
````
## File: src/components/TemplatePageSchema.astro
````astro
1: ---
2: /**
3: * TemplatePageSchema - Generates DigitalDocument schema for template download pages
4: *
5: * No visual output - only structured data in
````
## File: src/components/TrustedBySection.astro
````astro
1: ---
2: interface Props {
3: title?: string
4: description?: string
5: background?: 'primary' | 'white'
6: }
7:
8: const {
9: title = 'Trusted by Australian businesses',
10: description = 'From local cafes to national retail chains, thousands of businesses rely on RosterElf for rostering, HR and payroll.',
11: background = 'white',
12: } = Astro.props
13:
14: const bgClass = background === 'primary' ? 'bg-primary-100' : 'bg-white'
15: const fadeFromClass = background === 'primary' ? 'from-primary-100' : 'from-white'
16:
17: // Logo config with individual scale adjustments for visual weight normalization
18: // scale: 1 = baseline, <1 = reduce for heavy logos, >1 = increase for thin logos
19: // yOffset: vertical adjustment for wordmark baseline alignment (positive = down)
20: const logos = [
21: { src: '/images/clients/inspire-care-sa-logo.webp', alt: 'Inspire Care SA', scale: 1.1, yOffset: 0 },
22: { src: '/images/clients/pressed-earth-logo.webp', alt: 'Pressed Earth', scale: 0.85, yOffset: 0 },
23: { src: '/images/clients/toowoomba-catholic-logo.webp', alt: 'Toowoomba Catholic Kindergartens', scale: 1.0, yOffset: 0 },
24: { src: '/images/clients/cellarbration-logo.webp', alt: 'Cellarbrations', scale: 1.05, yOffset: 2 },
25: { src: '/images/clients/iga-logo.webp', alt: 'IGA', scale: 0.8, yOffset: 0 },
26: { src: '/images/clients/the-coffee-club-logo.webp', alt: 'The Coffee Club', scale: 0.85, yOffset: 0 },
27: { src: '/images/clients/glouster-park-logo.webp', alt: 'Gloucester Park', scale: 1.0, yOffset: 0 },
28: { src: '/images/clients/bagel-os-logo.webp', alt: "Bagel O's", scale: 0.9, yOffset: 0 },
29: { src: '/images/clients/bush-kids-childcare-logo.webp', alt: 'Bush Kids Childcare', scale: 1.0, yOffset: 0 },
30: { src: '/images/clients/the-embassy-logo.webp', alt: 'The Embassy', scale: 0.95, yOffset: 0 },
31: ]
32: ---
33:
34:
35:
36:
37:
{title}
38:
{description}
39:
40:
41:
42:
43:
44:
45: {
46: logos.map((logo) => (
47:
48:
57:
58: ))
59: }
60:
61: {
62: logos.map((logo) => (
63:
64:
73:
74: ))
75: }
76:
77:
78:
79:
80:
81:
82:
83:
84:
````
## File: src/components/VideoSchema.astro
````astro
1: ---
2: /**
3: * VideoSchema - Generates schema.org VideoObject markup for video pages
4: *
5: * This component adds structured data to help Google show video rich results
6: * in search. Required for videos to appear in Google Video search and carousels.
7: *
8: * @see https://developers.google.com/search/docs/appearance/structured-data/video
9: */
10:
11: interface Props {
12: name: string // Video title
13: description: string // Video description
14: videoId: string // YouTube video ID
15: uploadDate?: string // ISO 8601 datetime (e.g., "2024-01-15T00:00:00+10:00")
16: duration?: string // ISO 8601 duration (e.g., "PT2M30S" for 2 min 30 sec)
17: }
18:
19: const { name, description, videoId, uploadDate = '2024-01-01T00:00:00+10:00', duration } = Astro.props
20:
21: const videoSchema = {
22: '@context': 'https://schema.org',
23: '@type': 'VideoObject',
24: name: name,
25: description: description,
26: thumbnailUrl: [`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, `https://img.youtube.com/vi/${videoId}/sddefault.jpg`, `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`],
27: uploadDate: uploadDate,
28: ...(duration && { duration: duration }),
29: contentUrl: `https://www.youtube.com/watch?v=${videoId}`,
30: embedUrl: `https://www.youtube.com/embed/${videoId}`,
31: publisher: {
32: '@type': 'Organization',
33: name: 'RosterElf',
34: url: 'https://www.rosterelf.com',
35: logo: {
36: '@type': 'ImageObject',
37: url: 'https://www.rosterelf.com/images/logos/rosterelf-logo.svg',
38: },
39: },
40: }
41: ---
42:
43:
````
## File: src/components/WhyRosterelfSection.astro
````astro
1: ---
2: import { DollarSign, Shield, Headphones } from 'lucide-astro'
3:
4: interface Props {
5: chip?: string
6: title?: string
7: description?: string
8: background?: 'white' | 'primary'
9: }
10:
11: const { chip = 'WHY ROSTERELF', title = 'What sets RosterElf apart', description = 'Three reasons Australian businesses choose RosterElf over the competition.', background = 'white' } = Astro.props
12:
13: const bgClass = background === 'white' ? 'bg-white' : 'bg-primary-100'
14: const iconBgClass = background === 'white' ? 'bg-primary-100' : 'bg-white'
15: ---
16:
17:
18:
19:
20:
{chip}
21:
{title}
22:
{description}
23:
24:
25:
26:
27:
28:
29:
30:
One plan, all features
31:
32: No confusing tiers or expensive add-ons. RosterElf includes rostering, time tracking, payroll integration, HR tools, and support in one transparent price.
33:
34:
35:
36:
37:
38:
39:
40:
Built for Australian compliance
41:
42: Award interpretation, Fair Work compliance, penalty rates, and overtime calculations built specifically for Australian businesses. Not retrofitted from overseas platforms.
43:
44:
45:
46:
47:
48:
49:
50:
Real Australian support
51:
Talk to real people who understand Australian business. Free onboarding, phone support, and hands-on help — not chatbots or overseas call centres.
52:
53:
54:
55:
````
## File: src/layouts/BaseLayout.astro
````astro
1: ---
2: import '../styles/global.css'
3: import Header from '../components/Header.astro'
4: import Footer from '../components/Footer.astro'
5: import PageTracker from '../components/PageTracker.astro'
6: import GeoLocationModal from '../components/GeoLocationModal.astro'
7: import { hasUkVariant, getGlobalPath, isUkPath } from '../utils/ukPages'
8:
9: interface Props {
10: title: string
11: pageTitle?: string
12: ogTitle?: string
13: ogDescription?: string
14: description?: string
15: heroImage?: string
16: ogImage?: string
17: robots?: string
18: country?: 'uk' | null
19: }
20:
21: const { title, pageTitle, description, heroImage, ogImage = heroImage || '/images/hero-desktop.webp', robots, country: countryProp } = Astro.props
22: const browserTitle = pageTitle || title
23: const defaultDescription =
24: "RosterElf is Australia's best-rated rostering & HR software. Create staff schedules, track time and attendance, manage leave, and integrate with Xero payroll. Free 14-day trial."
25: const metaDescription = description || defaultDescription
26:
27: // Only load Google scripts in production environment
28: // Use process.env for shell environment variables set during build (IS_PRODUCTION=true npm run build)
29: const isProduction = process.env.IS_PRODUCTION === 'true'
30: const enableAnalytics = isProduction
31:
32: // Apply noindex only in non-production environments (unless explicitly set via robots prop)
33: const robotsContent = robots || (isProduction ? 'index, follow' : 'noindex, nofollow')
34:
35: // Detect country from URL
36: const pathname = Astro.url.pathname
37: const isUk = isUkPath(pathname)
38: const country = countryProp ?? (isUk ? 'uk' : null)
39: const globalPath = getGlobalPath(pathname)
40: const hasUkVersion = await hasUkVariant(globalPath)
41:
42: // Base URL for constructing absolute URLs
43: const baseURL = (Astro.site || 'https://www.rosterelf.com').toString().replace(/\/$/, '')
44: const ogImageURL = ogImage.startsWith('http') ? ogImage : baseURL + ogImage
45:
46: // og:locale — dynamic based on country
47: const ogLocale = country === 'uk' ? 'en_GB' : 'en_AU'
48:
49: // Canonical URL logic:
50: // - UK pages WITH dedicated UK version → canonical to self (UK URL)
51: // - UK pages WITHOUT dedicated UK version (catch-all) → canonical to AU version
52: // - AU pages → canonical to self
53: let canonicalURL: string
54: if (isUk && !hasUkVersion) {
55: // UK catch-all page: canonical points to AU version
56: canonicalURL = baseURL + (globalPath === '/' ? '' : globalPath)
57: } else {
58: // Dedicated page: canonical points to self
59: const canonicalPath = pathname.replace(/\/$/, '') || ''
60: canonicalURL = baseURL + canonicalPath
61: }
62:
63: // Generate hreflang tags (only for pages with dedicated UK variants)
64: const hreflangLinks: { hreflang: string; href: string }[] = []
65:
66: if (hasUkVersion) {
67: // Only emit hreflang when there's a dedicated UK version
68: const globalUrl = baseURL + (globalPath === '/' ? '' : globalPath)
69: const ukUrl = baseURL + '/uk' + (globalPath === '/' ? '' : globalPath)
70:
71: // x-default points to global (AU) version
72: hreflangLinks.push({ hreflang: 'x-default', href: globalUrl })
73: // en-AU points to global version
74: hreflangLinks.push({ hreflang: 'en-AU', href: globalUrl })
75: // en-GB points to UK version
76: hreflangLinks.push({ hreflang: 'en-GB', href: ukUrl })
77: }
78: ---
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96: {browserTitle}
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109: {hreflangLinks.map(({ hreflang, href }) => )}
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120: {
121: enableAnalytics && (
122: <>
123: {/* Google Consent Mode v2 - MUST be first before any Google scripts */}
124:
128:
129: {/* Google Ads Tag */}
130:
131:
132:
133: {/* Google Tag Manager */}
134:
138: >
139: )
140: }
141:
142:
143:
204:
205:
206:
207:
208:
225:
226:
227:
228:
229: {
230: enableAnalytics && (
231: <>
232: {/* Google Tag Manager (noscript) */}
233:
234:
235:
236: >
237: )
238: }
239:
240:
241:
242:
243:
244:
245:
246:
461:
462:
463:
550:
551:
584:
585:
````
## File: src/layouts/BlogLayout.astro
````astro
1: ---
2: /**
3: * BlogLayout - Standard layout for all blog posts
4: *
5: * HEADING STRUCTURE:
6: * - H1: Article title (provided by this layout from articleMeta.title)
7: * - H2: Main sections (e.g., "Why X matters", "Methods for Y", "FAQs")
8: * - H3: Subsections and card titles within H2 sections
9: * - DO NOT use H4 - keep hierarchy flat: H1 → H2 → H3
10: * - Keywords should appear naturally in H1 and key H2s, don't over-optimize
11: *
12: * IMAGE REQUIREMENTS (Unsplash):
13: * - ALWAYS download images from Unsplash before using (don't hotlink)
14: * - Save to /public/images/stock/ with naming: unsplash-{photo-id}-{description}.webp
15: * - Convert to WebP format for performance
16: * - Include photographer attribution in a comment near the image or in page frontmatter
17: * - Unsplash license: https://unsplash.com/license (free for commercial use, no attribution required but appreciated)
18: *
19: * PREMIERE GATING:
20: * - For scheduled posts, wrap the BlogLayout usage in PremiereGate component
21: * - Add premiereDate to articleMeta for filtering in listings
22: * - Use blogUtils.ts functions to filter unpublished posts from indexes/related sections
23: * - Example: import { filterPublishedPosts } from '../utils/blogUtils'
24: *
25: * CTA STRATEGY (for long-form 9+ min reads):
26: * 1. IN-ARTICLE CTA: Each blog should include a contextual CTA within the article content.
27: * This should be topic-specific and placed after the main content sections.
28: * Example: "Improve staff communication with RosterElf" for communication articles.
29: *
30: * 2. BOTTOM CTA: Provided by this layout via ctaTitle/ctaDescription props.
31: * This is a final conversion push for users who read to the end.
32: * Should be broader/benefit-focused.
33: *
34: * Both CTAs serve different purposes:
35: * - In-article = contextual, catches users mid-scroll
36: * - Bottom = final push, catches completers
37: */
38: import BaseLayout from './BaseLayout.astro'
39: import Breadcrumb from '../components/Breadcrumb.astro'
40: import TOCSidebar from '../components/TOCSidebar.astro'
41: import TrialButton from '../components/TrialButton.astro'
42: import FreeToolsGrid from '../components/FreeToolsGrid.astro'
43: import { Calendar, Clock, User, ArrowLeft } from 'lucide-astro'
44: import { isPublished, filterPublishedPosts, sortPostsByDate } from '../utils/blogUtils'
45:
46: interface TOCItem {
47: label: string
48: href: string
49: }
50:
51: interface RelatedPost {
52: title: string
53: excerpt: string
54: category: string
55: date: string
56: readTime: string
57: image: string
58: href: string
59: premiereDate?: string // ISO format (e.g., '2026-01-15') - if set and in future, post won't display
60: }
61:
62: interface ArticleMeta {
63: title: string
64: seoTitle?: string // SEO-optimized title tag (if different from H1 title, incorporates core keyword + page intent)
65: excerpt: string
66: category: string
67: categorySlug: string
68: author: string
69: authorImage: string
70: authorBio: string
71: authorUrl?: string // Optional link to author page (e.g., /author/steve-harris)
72: editor?: string // Optional editor name
73: editorImage?: string // Optional editor photo
74: editorBio?: string // Optional editor bio
75: editorUrl?: string // Optional link to editor page
76: date: string // Published date in format: "15 January 2025"
77: dateISO?: string // ISO 8601 format for schema: "2025-01-15" (optional, will parse from date if missing)
78: dateModified?: string // ISO 8601 format: "2025-01-20" (omit if same as published)
79: readTime: string
80: image: string
81: imageAlt?: string // Alt text for featured image (defaults to title)
82: imageWidth?: number // For OG/schema (defaults to 1200)
83: imageHeight?: number // For OG/schema (defaults to 630)
84: }
85:
86: interface Props {
87: articleMeta: ArticleMeta
88: tocItems: TOCItem[]
89: relatedPosts?: RelatedPost[] // Optional - will auto-populate if not provided or invalid
90: slug: string
91: ctaTitle?: string
92: ctaDescription?: string
93: faqSchema?: object
94: }
95:
96: const {
97: articleMeta,
98: tocItems,
99: relatedPosts = [],
100: slug,
101: ctaTitle = 'Ready to streamline your workforce management?',
102: ctaDescription = 'Join Australian businesses using RosterElf to simplify rostering, track time, and stay compliant.',
103: faqSchema,
104: } = Astro.props
105:
106: // Build lookup map of blog slug -> {meta, premiereDate} from actual blog files
107: const blogModules = import.meta.glob('../pages/blog/*.astro', { eager: true }) as Record
108: const blogDataLookup: Record = {}
109: for (const [path, mod] of Object.entries(blogModules)) {
110: const blogSlug = path.split('/').pop()?.replace('.astro', '') || ''
111: if (blogSlug && blogSlug !== 'index' && !blogSlug.startsWith('category') && mod.articleMeta) {
112: blogDataLookup[blogSlug] = {
113: meta: mod.articleMeta,
114: premiereDate: mod.premiereDate,
115: }
116: }
117: }
118:
119: // Filter provided related posts to only include existing, published blogs
120: const validRelatedPosts = relatedPosts.filter((post) => {
121: const postSlug = post.href.split('/').pop() || ''
122: const blogData = blogDataLookup[postSlug]
123: if (!blogData) return false // Blog doesn't exist
124: const premiereDate = post.premiereDate || blogData.premiereDate
125: return !premiereDate || isPublished(premiereDate)
126: })
127:
128: // If no valid related posts provided, auto-generate from same category
129: let publishedRelatedPosts: RelatedPost[]
130: if (validRelatedPosts.length >= 3) {
131: publishedRelatedPosts = validRelatedPosts.slice(0, 3)
132: } else {
133: // Auto-populate from published blogs (prioritize same category)
134: const allBlogs: RelatedPost[] = Object.entries(blogDataLookup)
135: .filter(([blogSlug, data]) => {
136: if (blogSlug === slug) return false // Exclude current post
137: return !data.premiereDate || isPublished(data.premiereDate)
138: })
139: .map(([blogSlug, data]) => ({
140: title: data.meta.title,
141: excerpt: data.meta.excerpt,
142: category: data.meta.category,
143: date: data.meta.date,
144: readTime: data.meta.readTime,
145: image: data.meta.image,
146: href: `/blog/${blogSlug}`,
147: premiereDate: data.premiereDate,
148: }))
149:
150: // Separate same category and other
151: const sameCategory = allBlogs.filter((p) => p.category === articleMeta.category)
152: const otherCategory = allBlogs.filter((p) => p.category !== articleMeta.category)
153:
154: // Sort by date (newest first) and combine
155: const sortedSame = sortPostsByDate(sameCategory)
156: const sortedOther = sortPostsByDate(otherCategory)
157:
158: publishedRelatedPosts = [...sortedSame, ...sortedOther].slice(0, 3)
159: }
160:
161: // Parse human-readable date to ISO format if not provided
162: // Supports formats: "15 January 2025", "January 15, 2025", etc.
163: function parseDateToISO(dateStr: string): string {
164: try {
165: const parsed = new Date(dateStr)
166: if (!isNaN(parsed.getTime())) {
167: return parsed.toISOString().split('T')[0]
168: }
169: } catch {
170: // Fall through to return current date
171: }
172: return new Date().toISOString().split('T')[0]
173: }
174:
175: // Use provided dateISO or parse from date string
176: const dateISO = articleMeta.dateISO || parseDateToISO(articleMeta.date)
177:
178: // Canonical URL for this article
179: const canonicalUrl = `https://www.rosterelf.com/blog/${slug}`
180:
181: // Image URL (ensure absolute)
182: const imageUrl = articleMeta.image.startsWith('http') ? articleMeta.image : `https://www.rosterelf.com${articleMeta.image}`
183:
184: // Default image dimensions for OG
185: const imageWidth = articleMeta.imageWidth || 1200
186: const imageHeight = articleMeta.imageHeight || 630
187:
188: // Author schema - can be reused
189: const authorSchema = {
190: '@type': 'Person',
191: name: articleMeta.author,
192: image: articleMeta.authorImage.startsWith('http') ? articleMeta.authorImage : `https://www.rosterelf.com${articleMeta.authorImage}`,
193: description: articleMeta.authorBio,
194: ...(articleMeta.authorUrl && { url: `https://www.rosterelf.com${articleMeta.authorUrl}` }),
195: }
196:
197: // Publisher schema (RosterElf organization)
198: const publisherSchema = {
199: '@type': 'Organization',
200: name: 'RosterElf',
201: url: 'https://www.rosterelf.com',
202: logo: {
203: '@type': 'ImageObject',
204: url: 'https://www.rosterelf.com/logo.png',
205: width: 512,
206: height: 512,
207: },
208: sameAs: ['https://www.facebook.com/raborosterelf', 'https://www.linkedin.com/company/rosterelf', 'https://twitter.com/raborosterelf'],
209: }
210:
211: // Full Article schema (optimized for Google)
212: const articleSchema = {
213: '@context': 'https://schema.org',
214: '@type': 'Article',
215: headline: articleMeta.title,
216: description: articleMeta.excerpt,
217: author: authorSchema,
218: publisher: publisherSchema,
219: datePublished: dateISO,
220: ...(articleMeta.dateModified && { dateModified: articleMeta.dateModified }),
221: mainEntityOfPage: {
222: '@type': 'WebPage',
223: '@id': canonicalUrl,
224: },
225: image: {
226: '@type': 'ImageObject',
227: url: imageUrl,
228: width: imageWidth,
229: height: imageHeight,
230: },
231: url: canonicalUrl,
232: articleSection: articleMeta.category,
233: inLanguage: 'en-AU',
234: isAccessibleForFree: true,
235: keywords: articleMeta.category, // Can be enhanced with actual keywords
236: }
237: ---
238:
239:
246:
247:
248:
249:
256:
257:
258:
259: {articleMeta.category}
260:
261: {articleMeta.title}
262: {articleMeta.excerpt}
263:
264:
265:
266: {
267: articleMeta.authorUrl ? (
268: <>
269: Written by{' '}
270:
271: {articleMeta.author}
272:
273: >
274: ) : (
275: <>Written by {articleMeta.author}>
276: )
277: }
278:
279: {
280: articleMeta.editor && (
281:
282: {articleMeta.editorUrl ? (
283: <>
284: Edited by{' '}
285:
286: {articleMeta.editor}
287:
288: >
289: ) : (
290: <>Edited by {articleMeta.editor}>
291: )}
292:
293: )
294: }
295:
296:
297: {articleMeta.date}
298:
299:
300:
301: {articleMeta.readTime}
302:
303:
304:
305:
306:
307:
308:
309:
310:
311:
312:
313:
314:
315:
316:
317:
318:
319:
320:
321:
322:
323:
324:
325:
326:
327: {
328: articleMeta.authorUrl ? (
329:
330:
331:
332: ) : (
333:
334: )
335: }
336:
337: {
338: articleMeta.authorUrl ? (
339:
340: {articleMeta.author}
341:
342: ) : (
343:
{articleMeta.author}
344: )
345: }
346:
{articleMeta.authorBio}
347:
348:
349:
350:
351:
352:
353: {
354: articleMeta.editor && (
355:
356:
357:
358: {articleMeta.editorUrl ? (
359:
360:
361:
362: ) : (
363:
364: )}
365:
366: {articleMeta.editorUrl ? (
367:
368: {articleMeta.editor}
369:
370: ) : (
371:
{articleMeta.editor}
372: )}
373:
{articleMeta.editorBio || 'Editor'}
374:
375:
376:
377:
378: )
379: }
380:
381:
382:
388:
389:
390:
391:
392:
393: {
394: publishedRelatedPosts.length > 0 && (
395:
396:
397:
Related articles
398:
399:
423:
424:
425: )
426: }
427:
428:
429:
430:
431:
{ctaTitle}
432:
{ctaDescription}
433:
444:
445:
446:
447:
448: {faqSchema && }
449:
450:
457:
````
## File: src/layouts/CategoryLandingLayout.astro
````astro
1: ---
2: import BaseLayout from './BaseLayout.astro'
3: import Breadcrumb from '../components/Breadcrumb.astro'
4: import SupportSection from '../components/SupportSection.astro'
5: import FAQSection from '../components/FAQSection.astro'
6: import { ChevronRight, BookOpen, Phone, Mail, Video } from 'lucide-astro'
7:
8: interface Article {
9: title: string
10: description: string
11: href: string
12: permissionLevel: string
13: device: string
14: updatedDate: string
15: }
16:
17: interface RelatedCategory {
18: title: string
19: description: string
20: href: string
21: icon: any
22: }
23:
24: interface CategoryFaq {
25: question: string
26: answer: string
27: }
28:
29: interface CategoryInfo {
30: title: string
31: slug: string
32: description: string
33: icon: any
34: }
35:
36: interface Props {
37: categoryInfo: CategoryInfo
38: articles: Article[]
39: relatedCategories: RelatedCategory[]
40: categoryFaqs?: CategoryFaq[]
41: }
42:
43: const { categoryInfo, articles, relatedCategories, categoryFaqs } = Astro.props
44:
45: // Default FAQs if none provided
46: const defaultFaqs: CategoryFaq[] = [
47: {
48: question: 'How do I find a specific article?',
49: answer: 'Use the search bar at the top of the knowledge base or browse articles by category. Each category page lists all available articles with descriptions.',
50: },
51: {
52: question: 'Can I access these articles on mobile?',
53: answer: 'Yes, all knowledge base articles are fully responsive and optimized for mobile viewing. You can access them from any device.',
54: },
55: {
56: question: 'How often are articles updated?',
57: answer: 'We regularly update articles to reflect new features and improvements. The last updated date is shown at the bottom of each article card.',
58: },
59: {
60: question: "What if I can't find what I'm looking for?",
61: answer: "Contact our Australian support team via email, phone, live chat, or book a one-on-one demo. We're here to help!",
62: },
63: ]
64:
65: const faqs = categoryFaqs || defaultFaqs
66:
67: const supportOptions = [
68: {
69: title: 'Knowledge base',
70: description: 'Browse all help articles',
71: href: '/support/knowledge-base',
72: icon: BookOpen,
73: internal: true,
74: },
75: {
76: title: 'Book a call',
77: description: 'Book a one-on-one support session',
78: href: 'https://calendly.com/d/23c-82n-q9p/rosterelf-support',
79: icon: Phone,
80: },
81: {
82: title: 'Join our weekly webinar',
83: description: 'Learn tips and tricks each week',
84: href: 'https://us02web.zoom.us/webinar/register/WN_0Q2bbMrKSz2eDCDhNj8dIg',
85: icon: Video,
86: },
87: {
88: title: 'Contact us',
89: description: 'Get in touch with our support team',
90: href: '/contact',
91: icon: Mail,
92: internal: true,
93: },
94: ]
95:
96: // Schema
97: const collectionSchema = {
98: '@context': 'https://schema.org',
99: '@type': 'CollectionPage',
100: name: `${categoryInfo.title} Help Articles | RosterElf`,
101: description: categoryInfo.description,
102: url: `https://www.rosterelf.com/support/knowledge-base/${categoryInfo.slug}`,
103: inLanguage: 'en-AU',
104: dateModified: new Date().toISOString().split('T')[0],
105: mainEntity: {
106: '@type': 'ItemList',
107: name: `${categoryInfo.title} Articles`,
108: numberOfItems: articles.length,
109: itemListElement: articles.map((article, index) => ({
110: '@type': 'ListItem',
111: position: index + 1,
112: name: article.title,
113: description: article.description,
114: url: `https://www.rosterelf.com${article.href}`,
115: })),
116: },
117: breadcrumb: {
118: '@type': 'BreadcrumbList',
119: itemListElement: [
120: { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://www.rosterelf.com' },
121: { '@type': 'ListItem', position: 2, name: 'Support', item: 'https://www.rosterelf.com/support' },
122: { '@type': 'ListItem', position: 3, name: 'Knowledge Base', item: 'https://www.rosterelf.com/support/knowledge-base' },
123: { '@type': 'ListItem', position: 4, name: categoryInfo.title, item: `https://www.rosterelf.com/support/knowledge-base/${categoryInfo.slug}` },
124: ],
125: },
126: provider: {
127: '@type': 'Organization',
128: name: 'RosterElf',
129: url: 'https://www.rosterelf.com',
130: },
131: }
132:
133: const faqSchema = {
134: '@context': 'https://schema.org',
135: '@type': 'FAQPage',
136: mainEntity: faqs.map((faq) => ({
137: '@type': 'Question',
138: name: faq.question,
139: acceptedAnswer: {
140: '@type': 'Answer',
141: text: faq.answer,
142: },
143: })),
144: }
145: ---
146:
147:
148:
149:
150:
151:
152:
153:
154:
162:
163:
164:
165: {categoryInfo.logo ?
:
}
166:
167:
168:
SUPPORT
169:
{categoryInfo.title}
170:
{categoryInfo.description}
171:
{articles.length} articles
172:
173:
174:
175:
176:
188:
189:
190:
191:
197:
198:
199:
200:
212: Press Enter to ask Elf AI
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224: BROWSE ARTICLES
225:
All {categoryInfo.title.toLowerCase()} articles
226:
227:
228:
255:
256:
257:
258:
259:
260:
261:
262: RELATED GUIDES
263:
Explore related guides
264:
265:
266:
283:
284:
285:
286:
287:
288:
289:
290:
NEED MORE HELP?
291:
Still have questions?
292:
Our support team is here to help. Choose the option that works best for you.
293:
294:
295:
313:
314:
315:
316:
317:
318:
319:
320:
321:
410:
````
## File: src/layouts/ComparisonLayout.astro
````astro
1: ---
2: /**
3: * Reusable layout for software comparison pages
4: * Used for: Deputy vs Connecteam, Deputy vs RosterElf, etc.
5: */
6: import BaseLayout from './BaseLayout.astro'
7: import Breadcrumb from '../components/Breadcrumb.astro'
8: import TOCSidebar from '../components/TOCSidebar.astro'
9: import ReviewMethodologyCallout from '../components/ReviewMethodologyCallout.astro'
10: import FAQSection from '../components/FAQSection.astro'
11: import CTASection from '../components/CTASection.astro'
12:
13: interface BreadcrumbItem {
14: label: string
15: href: string
16: }
17:
18: interface TocItem {
19: label: string
20: href: string
21: }
22:
23: interface FaqItem {
24: question: string
25: answer: string
26: }
27:
28: interface FaqCategory {
29: name: string
30: faqs: FaqItem[]
31: }
32:
33: interface AuthorInfo {
34: name: string
35: image: string
36: role: string
37: bio: string
38: url: string
39: }
40:
41: interface Props {
42: // Page metadata
43: title: string
44: pageTitle: string
45: description: string
46: canonical?: string
47:
48: // Hero content
49: chipText?: string
50: heroTitle: string
51: heroSubtitle?: string
52: publishDate?: string
53: readTime?: string
54:
55: // Author
56: author?: AuthorInfo
57:
58: // Navigation
59: breadcrumbItems: BreadcrumbItem[]
60: tocItems: TocItem[]
61:
62: // FAQ
63: faqCategories: FaqCategory[]
64: faqChip?: string
65: faqTitle?: string
66:
67: // Schema
68: faqSchema?: object
69: comparisonSchema?: object
70:
71: // CTA
72: ctaBackground?: 'white' | 'primary' | 'gray'
73: }
74:
75: const {
76: title,
77: pageTitle,
78: description,
79: canonical,
80: chipText = 'SOFTWARE COMPARISONS',
81: heroTitle,
82: heroSubtitle,
83: publishDate,
84: readTime,
85: author = {
86: name: 'Steve Harris',
87: image: '/images/steve-harris-author.webp',
88: role: 'Written by',
89: bio: 'Steve Harris is a workforce management and HR strategy expert. He has spent over a decade advising businesses in hospitality, retail, healthcare, and other fast-paced industries.',
90: url: '/author/steve-harris',
91: },
92: breadcrumbItems,
93: tocItems,
94: faqCategories,
95: faqChip = 'FAQ',
96: faqTitle,
97: faqSchema,
98: comparisonSchema,
99: ctaBackground = 'white',
100: } = Astro.props
101:
102: // Generate FAQ title if not provided
103: const generatedFaqTitle = faqTitle || `${heroTitle.split(':')[0]} FAQ`
104: ---
105:
106:
711:
712:
713:
714:
715:
716:
717:
718:
{chipText}
719:
720: {heroTitle}
721: {heroSubtitle && {heroSubtitle} }
722:
723: {
724: (publishDate || readTime) && (
725:
726: {publishDate && {publishDate} }
727: {publishDate && readTime && • }
728: {readTime && {readTime} }
729:
730: )
731: }
732:
733:
734: {
735: author && (
736:
737:
738:
739:
{author.role}
740:
{author.name}
741:
742: {/* Tooltip */}
743:
751:
752: )
753: }
754:
755:
756:
757:
758:
759:
760:
761:
762:
763:
764:
765:
766:
767:
768:
769:
770:
771:
772:
773:
774:
775: {faqSchema && }
776: {comparisonSchema && }
777:
````
## File: src/layouts/GlossaryCategoryLayout.astro
````astro
1: ---
2: import BaseLayout from './BaseLayout.astro'
3: import Breadcrumb from '../components/Breadcrumb.astro'
4: import PopularIndustriesSection from '../components/PopularIndustriesSection.astro'
5: import TrialButton from '../components/TrialButton.astro'
6: import { ChevronRight } from 'lucide-astro'
7: import { isPageVisible } from '../utils/premiereDate'
8:
9: interface Term {
10: title: string
11: slug: string
12: definition: string
13: premiereDate?: string
14: }
15:
16: interface RelatedCategory {
17: name: string
18: slug: string
19: }
20:
21: interface Props {
22: title: string
23: slug: string
24: description: string
25: icon: string
26: terms: Term[]
27: relatedCategories?: RelatedCategory[]
28: }
29:
30: const { title, slug, description, icon, terms, relatedCategories = [] } = Astro.props
31:
32: // Filter terms by premiere date
33: const visibleTerms = terms.filter((term) => term.premiereDate && isPageVisible(term.premiereDate))
34: const comingSoonTerms = terms.filter((term) => !term.premiereDate || !isPageVisible(term.premiereDate))
35:
36: // Build canonical URL
37: const baseURL = 'https://www.rosterelf.com'
38: const canonicalURL = `${baseURL}/glossary/${slug}`
39:
40: // Schema: BreadcrumbList
41: const breadcrumbSchema = {
42: '@context': 'https://schema.org',
43: '@type': 'BreadcrumbList',
44: itemListElement: [
45: {
46: '@type': 'ListItem',
47: position: 1,
48: name: 'Home',
49: item: baseURL,
50: },
51: {
52: '@type': 'ListItem',
53: position: 2,
54: name: 'HR Glossary',
55: item: `${baseURL}/glossary`,
56: },
57: {
58: '@type': 'ListItem',
59: position: 3,
60: name: title,
61: item: canonicalURL,
62: },
63: ],
64: }
65:
66: // Schema: CollectionPage
67: const collectionSchema = {
68: '@context': 'https://schema.org',
69: '@type': 'CollectionPage',
70: name: title,
71: description: description,
72: url: canonicalURL,
73: isPartOf: {
74: '@type': 'WebSite',
75: name: 'RosterElf',
76: url: baseURL,
77: },
78: about: {
79: '@type': 'DefinedTermSet',
80: name: `${title} - Australian HR Glossary`,
81: description: description,
82: },
83: }
84:
85: // Schema: ItemList (for visible terms)
86: const itemListSchema =
87: visibleTerms.length > 0
88: ? {
89: '@context': 'https://schema.org',
90: '@type': 'ItemList',
91: name: `${title} Terms`,
92: description: `Glossary terms in the ${title} category`,
93: numberOfItems: visibleTerms.length,
94: itemListElement: visibleTerms.map((term, index) => ({
95: '@type': 'ListItem',
96: position: index + 1,
97: name: term.title,
98: url: `${baseURL}/glossary/${term.slug}`,
99: })),
100: }
101: : null
102:
103: // Generate concise title for SEO (under 60 chars)
104: const pageTitle = `${title} | RosterElf`
105: const metaDescription = `${description} Browse ${terms.length} Australian HR definitions in the ${title} category.`
106: ---
107:
108:
109:
110:
111:
112: {itemListSchema && }
113:
114:
115:
116:
117:
118:
119:
126:
127:
128:
129:
130:
131: {icon}
132:
133:
{title}
134:
135:
136:
{description}
137:
138:
139: {
140: Astro.slots.has('default') && (
141:
142:
143:
144: )
145: }
146:
147:
148:
149: {visibleTerms.length}
150: {visibleTerms.length === 1 ? 'term' : 'terms'} published
151: {comingSoonTerms.length > 0 && ({comingSoonTerms.length} coming soon) }
152:
153:
154:
155:
156:
157:
158:
159:
Browse {title.toLowerCase()} terms
160:
161: {
162: visibleTerms.length > 0 && (
163:
171: )
172: }
173:
174: {
175: comingSoonTerms.length > 0 && (
176: <>
177:
Coming soon
178:
179: {comingSoonTerms.map((term) => (
180:
181:
{term.title}
182:
{term.definition}
183:
184: ))}
185:
186: >
187: )
188: }
189:
190:
191:
192:
193: {
194: relatedCategories.length > 0 && (
195:
196:
197:
Related categories
198:
209:
210:
211: )
212: }
213:
214:
215:
216:
217:
218:
219:
220:
221:
Simplify your workforce management
222:
223: RosterElf helps Australian businesses manage rosters, track time and attendance, and stay compliant with Fair Work requirements. Try it free for 14 days.
224:
225:
236:
237:
238:
239:
````
## File: src/layouts/GlossaryTermLayout.astro
````astro
1: ---
2: import BaseLayout from './BaseLayout.astro'
3: import Breadcrumb from '../components/Breadcrumb.astro'
4: import DisclaimerSection from '../components/DisclaimerSection.astro'
5: import TOCSidebar from '../components/TOCSidebar.astro'
6: import GlossarySupportLinks from '../components/GlossarySupportLinks.astro'
7: import GlossaryRelatedResources from '../components/GlossaryRelatedResources.astro'
8: import { ArrowRight, Clock, ChevronRight, Plus, Minus } from 'lucide-astro'
9: import { getAuthorByCategory, type GlossaryAuthor } from '../data/glossaryAuthors'
10: import { glossaryToSupportMapping } from '../data/glossaryToSupportMapping'
11: import CTASection from '../components/CTASection.astro'
12: import TrialButton from '../components/TrialButton.astro'
13:
14: interface FAQ {
15: question: string
16: answer: string
17: }
18:
19: interface TOCItem {
20: label: string
21: href: string
22: }
23:
24: interface Props {
25: term: string
26: slug: string
27: definition: string
28: category: string
29: categorySlug: string
30: metaDescription: string
31: publishedDate: string
32: updatedDate?: string
33: relatedArticles?: Array<{ title: string; href: string }>
34: relatedTerms?: Array<{ term: string; slug?: string }>
35: supportArticles?: Array<{ title: string; href: string; type?: 'setup' | 'integration' | 'advanced' }>
36: inlineCTA?: {
37: text: string
38: href: string
39: }
40: faqs?: FAQ[]
41: isPlural?: boolean
42: articleType?: 'a' | 'an' | 'none' | 'plural'
43: tocItems?: TOCItem[]
44: }
45:
46: const {
47: term,
48: slug,
49: definition,
50: category,
51: categorySlug,
52: metaDescription,
53: publishedDate,
54: updatedDate,
55: relatedArticles = [],
56: relatedTerms = [],
57: supportArticles,
58: inlineCTA,
59: faqs = [],
60: isPlural = false,
61: articleType,
62: tocItems = [],
63: } = Astro.props
64:
65: // Auto-lookup support articles from centralized mapping if not provided
66: const resolvedSupportArticles = supportArticles || glossaryToSupportMapping[slug] || []
67:
68: // Check if TOC should be shown
69: const showTOC = tocItems.length > 0
70:
71: // Title prefix based on articleType (or fallback to isPlural for backward compatibility)
72: const titlePrefix =
73: articleType === 'plural' ? 'What are' : articleType === 'an' ? 'What is an' : articleType === 'none' ? 'What is' : articleType === 'a' ? 'What is a' : isPlural ? 'What are' : 'What is a'
74:
75: // Get author based on category
76: const author: GlossaryAuthor = getAuthorByCategory(categorySlug)
77:
78: // Calculate read time (rough estimate based on content)
79: const readTime = '5 min read'
80:
81: // Format dates
82: const formatDate = (dateStr: string) => {
83: const date = new Date(dateStr)
84: return date.toLocaleDateString('en-AU', { day: 'numeric', month: 'short', year: 'numeric' })
85: }
86:
87: // Build canonical URL
88: const baseURL = 'https://www.rosterelf.com'
89: const canonicalURL = `${baseURL}/glossary/${slug}`
90:
91: // Schema: BreadcrumbList
92: const breadcrumbSchema = {
93: '@context': 'https://schema.org',
94: '@type': 'BreadcrumbList',
95: itemListElement: [
96: {
97: '@type': 'ListItem',
98: position: 1,
99: name: 'Home',
100: item: baseURL,
101: },
102: {
103: '@type': 'ListItem',
104: position: 2,
105: name: 'HR Glossary',
106: item: `${baseURL}/glossary`,
107: },
108: {
109: '@type': 'ListItem',
110: position: 3,
111: name: category,
112: item: `${baseURL}/glossary/${categorySlug}`,
113: },
114: {
115: '@type': 'ListItem',
116: position: 4,
117: name: term,
118: item: canonicalURL,
119: },
120: ],
121: }
122:
123: // Schema: DefinedTerm
124: const definedTermSchema = {
125: '@context': 'https://schema.org',
126: '@type': 'DefinedTerm',
127: name: term,
128: description: definition,
129: inDefinedTermSet: {
130: '@type': 'DefinedTermSet',
131: name: 'RosterElf Australian HR Glossary',
132: url: `${baseURL}/glossary`,
133: },
134: }
135:
136: // Schema: Article with author
137: const articleSchema = {
138: '@context': 'https://schema.org',
139: '@type': 'Article',
140: headline: `${titlePrefix} ${term}?`,
141: description: metaDescription,
142: image: `${baseURL}/images/og-glossary.webp`,
143: datePublished: publishedDate,
144: dateModified: updatedDate || publishedDate,
145: author: {
146: '@type': author.name === 'RosterElf Team' ? 'Organization' : 'Person',
147: name: author.name,
148: url: `${baseURL}${author.href}`,
149: ...(author.name !== 'RosterElf Team' && { jobTitle: author.role }),
150: },
151: publisher: {
152: '@type': 'Organization',
153: name: 'RosterElf',
154: logo: {
155: '@type': 'ImageObject',
156: url: `${baseURL}/images/logo.png`,
157: },
158: },
159: mainEntityOfPage: {
160: '@type': 'WebPage',
161: '@id': canonicalURL,
162: },
163: }
164:
165: // Schema: FAQPage (if FAQs provided)
166: const faqSchema =
167: faqs.length > 0
168: ? {
169: '@context': 'https://schema.org',
170: '@type': 'FAQPage',
171: mainEntity: faqs.map((faq) => ({
172: '@type': 'Question',
173: name: faq.question,
174: acceptedAnswer: {
175: '@type': 'Answer',
176: text: faq.answer,
177: },
178: })),
179: }
180: : null
181:
182: // Generate concise title for SEO (under 60 chars)
183: // Use simpler format that's more scannable and keyword-focused
184: const pageTitle = term.length > 35 ? `${term} | RosterElf` : `${term} | Australian HR Glossary`
185: ---
186:
187:
188:
189:
190:
191:
192: {faqSchema && }
193:
194:
195:
196:
197:
198:
199:
207:
208:
209:
210:
211: {category}
212:
213:
214:
{titlePrefix} {term} ?
215:
216:
217:
218: Updated {formatDate(updatedDate || publishedDate)}
219: •
220:
221:
222: {readTime}
223:
224:
225:
226:
227:
230:
231:
232:
233:
234:
235:
236:
237: {showTOC &&
}
238:
239:
240:
241:
242:
243:
244: {
245: inlineCTA && (
246:
247:
{inlineCTA.text}
248:
249:
250: )
251: }
252:
253:
254:
255:
256:
257:
258: {
259: faqs.length > 0 && (
260:
261:
262:
Frequently asked questions
263:
264: {faqs.map((faq, index) => (
265:
266:
272: {faq.question}
273:
274:
275:
276:
279:
280: ))}
281:
282:
283:
284: )
285: }
286:
287:
288:
289:
290:
291:
292:
293:
298:
299:
300:
301:
302:
303:
304: {resolvedSupportArticles.length > 0 && }
305:
306:
307:
308:
309:
310:
317:
318:
319:
324:
325:
326:
410:
411:
````
## File: src/layouts/ProductUpdateLayout.astro
````astro
1: ---
2: /**
3: * ProductUpdateLayout - Commercial layout for product update announcements
4: *
5: * STRUCTURE: All product update pages follow this consistent structure:
6: * 1. Breadcrumbs (white bg) - Auto-generated from articleMeta.title
7: * 2. Hero Section (green bg) - Shows title, date, author with tooltip
8: * 3. Main Content Area (white bg) - TOC sidebar + article content slot + FAQs
9: * 4. Related Features (green bg) - Conditional: if currentFeature provided
10: * 5. Back to Product Updates (white bg) - Always shown
11: * 6. Related Product Updates (green bg) - Conditional: if relatedUpdates provided
12: * 7. Related Support Articles (white bg) - Conditional: if relatedArticles provided
13: * 8. Latest Insights (green bg) - Always shown with focusBlogCategory
14: * 9. CTA Section (white bg) - Always shown with trial button + demo link
15: * 10. Back to Top Button - Always shown (fixed position)
16: *
17: * USAGE: Individual pages only need to:
18: * - Define articleMeta (title, description, dates, author with bio)
19: * - Define tocItems (must match H2 IDs exactly)
20: * - Define faqs array (optional)
21: * - Define slug for canonical URL
22: * - Provide related content props (currentFeature, relatedUpdates, relatedArticles)
23: * - Specify focusBlogCategory for Latest Insights
24: * - Write article content in slot (H2s with IDs + paragraphs)
25: *
26: * All components, backgrounds, spacing, and order are handled by this layout.
27: */
28: import BaseLayout from './BaseLayout.astro'
29: import Breadcrumb from '../components/Breadcrumb.astro'
30: import TOCSidebar from '../components/TOCSidebar.astro'
31: import TrialButton from '../components/TrialButton.astro'
32: import RelatedFeaturesSection from '../components/RelatedFeaturesSection.astro'
33: import RelatedProductUpdates from '../components/RelatedProductUpdates.astro'
34: import RelatedSupportArticles from '../components/RelatedSupportArticles.astro'
35: import LatestInsightsSection from '../components/LatestInsightsSection.astro'
36: import { Calendar, ArrowUp, ChevronRight, Sparkles } from 'lucide-astro'
37: import { getRecentUpdates } from '../utils/productUpdateUtils'
38:
39: interface TOCItem {
40: label: string
41: href: string
42: }
43:
44: interface FAQ {
45: question: string
46: answer: string
47: }
48:
49: interface ProductUpdate {
50: title: string
51: excerpt: string
52: href: string
53: date: string
54: tag: string
55: }
56:
57: interface SupportArticle {
58: title: string
59: excerpt: string
60: href: string
61: category?: string
62: }
63:
64: interface BreadcrumbItem {
65: label: string
66: href: string
67: }
68:
69: interface ArticleMeta {
70: title: string
71: description: string
72: category: string
73: categorySlug: string
74: updatedDate: string // Human readable: "2 February 2026"
75: updatedDateISO: string // ISO format: "2026-02-02"
76: author?: {
77: name: string
78: slug: string
79: photo: string
80: bio: string
81: href: string
82: }
83: }
84:
85: interface Props {
86: articleMeta: ArticleMeta
87: tocItems: TOCItem[]
88: faqs?: FAQ[]
89: slug: string
90: currentFeature?: string
91: relatedUpdates?: ProductUpdate[]
92: relatedArticles?: SupportArticle[]
93: focusBlogCategory?: string
94: hideDefaultCTA?: boolean
95: }
96:
97: const { articleMeta, tocItems, faqs = [], slug, currentFeature, relatedUpdates = [], relatedArticles = [], focusBlogCategory, hideDefaultCTA = false } = Astro.props
98:
99: // Get 3 most recent product updates (excluding current page)
100: const recentUpdates = getRecentUpdates(3, `/product-updates/${slug}`)
101:
102: // Build breadcrumb items for product updates
103: const breadcrumbItems: BreadcrumbItem[] = [
104: { label: 'Home', href: '/' },
105: { label: 'Product updates', href: '/product-updates' },
106: { label: articleMeta.title, href: `/product-updates/${slug}` },
107: ]
108:
109: // Canonical URL
110: const canonicalUrl = `https://www.rosterelf.com/product-updates/${slug}`
111:
112: // Publisher schema
113: const publisherSchema = {
114: '@type': 'Organization',
115: name: 'RosterElf',
116: url: 'https://www.rosterelf.com',
117: logo: {
118: '@type': 'ImageObject',
119: url: 'https://www.rosterelf.com/logo.png',
120: width: 512,
121: height: 512,
122: },
123: }
124:
125: // BreadcrumbList schema
126: const breadcrumbSchema = {
127: '@context': 'https://schema.org',
128: '@type': 'BreadcrumbList',
129: '@id': `${canonicalUrl}#breadcrumb`,
130: itemListElement: breadcrumbItems.map((item, index) => ({
131: '@type': 'ListItem',
132: position: index + 1,
133: name: item.label,
134: item: `https://www.rosterelf.com${item.href}`,
135: })),
136: }
137:
138: // Article schema (BlogPosting type for product announcements)
139: const articleSchema = {
140: '@context': 'https://schema.org',
141: '@type': 'BlogPosting',
142: headline: articleMeta.title,
143: description: articleMeta.description,
144: datePublished: articleMeta.updatedDateISO,
145: dateModified: articleMeta.updatedDateISO,
146: publisher: publisherSchema,
147: image: {
148: '@type': 'ImageObject',
149: url: 'https://www.rosterelf.com/logo.png',
150: width: 512,
151: height: 512,
152: },
153: ...(articleMeta.author && {
154: author: {
155: '@type': 'Person',
156: name: articleMeta.author.name,
157: url: `https://www.rosterelf.com/author/${articleMeta.author.slug}`,
158: image: `https://www.rosterelf.com${articleMeta.author.photo}`,
159: },
160: }),
161: mainEntityOfPage: {
162: '@type': 'WebPage',
163: '@id': canonicalUrl,
164: },
165: url: canonicalUrl,
166: articleSection: 'Product Updates',
167: inLanguage: 'en-AU',
168: isAccessibleForFree: true,
169: keywords: 'RosterElf, Product Updates, New Features, Workforce Management',
170: }
171:
172: // FAQ schema (if FAQs provided)
173: const faqSchema =
174: faqs.length > 0
175: ? {
176: '@context': 'https://schema.org',
177: '@type': 'FAQPage',
178: mainEntity: faqs.map((faq) => ({
179: '@type': 'Question',
180: name: faq.question,
181: acceptedAnswer: {
182: '@type': 'Answer',
183: text: faq.answer,
184: },
185: })),
186: }
187: : null
188:
189: // WebPage schema (for page-level information)
190: const webPageSchema = {
191: '@context': 'https://schema.org',
192: '@type': 'WebPage',
193: '@id': canonicalUrl,
194: url: canonicalUrl,
195: name: articleMeta.title,
196: description: articleMeta.description,
197: inLanguage: 'en-AU',
198: isPartOf: {
199: '@type': 'WebSite',
200: '@id': 'https://www.rosterelf.com/#website',
201: name: 'RosterElf',
202: url: 'https://www.rosterelf.com',
203: publisher: publisherSchema,
204: },
205: breadcrumb: {
206: '@type': 'BreadcrumbList',
207: '@id': `${canonicalUrl}#breadcrumb`,
208: },
209: datePublished: articleMeta.updatedDateISO,
210: dateModified: articleMeta.updatedDateISO,
211: }
212: ---
213:
214:
215:
216:
217:
218:
219: {faqSchema && }
220:
221:
222:
223:
224:
225:
226:
231:
232:
233:
234:
235:
236:
237:
238: New Feature
239:
240:
{articleMeta.title}
241:
242:
243: {articleMeta.updatedDate}
244:
245: {
246: articleMeta.author && (
247:
248:
249:
250:
Written by
251:
{articleMeta.author.name}
252:
253:
261:
262: )
263: }
264:
265:
266:
267:
268:
269:
270:
271:
272:
273:
276:
277:
278:
279:
280:
281:
282:
283:
284: {
285: faqs.length > 0 && (
286:
297: )
298: }
299:
300:
301:
302:
303:
304:
305: {currentFeature && }
306:
307:
308: {recentUpdates.length > 0 && }
309:
310:
311: {relatedArticles.length > 0 && }
312:
313:
314:
315:
316:
317:
318:
319:
320: {
321: !hideDefaultCTA && (
322:
323:
324:
Ready to try this feature?
325:
326: Join 30,000+ Australian businesses using RosterElf to save hours on scheduling and support compliance efforts. Start your free trial today—no credit card required.
327:
328:
339:
340:
341: )
342: }
343:
344:
345:
350:
351:
352:
353:
354:
355:
×
356:
357:
358:
359:
431:
432:
461:
````
## File: src/layouts/ReviewLayout.astro
````astro
1: ---
2: /**
3: * ReviewLayout - Standard layout for software review pages
4: *
5: * HEADING STRUCTURE:
6: * - H1: Review title (provided by this layout from reviewMeta.title)
7: * - H2: Main sections (Quick summary, Pricing, Rostering, etc.)
8: * - H3: Subsections within H2 (Pros & cons, What works well, etc.)
9: * - DO NOT use H4 - keep hierarchy flat: H1 → H2 → H3
10: *
11: * CONTENT STRUCTURE:
12: * Each review should cover these sections (in order):
13: * 1. Quick summary (with overall score)
14: * 2. Pricing and plans
15: * 3. Feature sections (Rostering, Time tracking, Payroll, etc.)
16: * 4. Use cases
17: * 5. User ratings
18: * 6. Security
19: * 7. Support
20: * 8. Final verdict
21: * 9. Review methodology
22: * 10. Related resources
23: *
24: * SCORING:
25: * - Overall score: 1-10 scale
26: * - Category scores: 1-10 scale (Rostering, Time tracking, Mobile, Pricing, Support, Security)
27: */
28: import BaseLayout from './BaseLayout.astro'
29: import Breadcrumb from '../components/Breadcrumb.astro'
30: import TOCSidebar from '../components/TOCSidebar.astro'
31: import ReviewMethodologyCallout from '../components/ReviewMethodologyCallout.astro'
32: import FAQSection from '../components/FAQSection.astro'
33: import SupportSection from '../components/SupportSection.astro'
34: import { ArrowRight } from 'lucide-astro'
35:
36: interface TOCItem {
37: label: string
38: href: string
39: }
40:
41: interface FAQCategory {
42: name: string
43: faqs: {
44: question: string
45: answer: string
46: }[]
47: }
48:
49: interface CategoryScore {
50: category: string
51: score: number
52: }
53:
54: interface RelatedResource {
55: tag: string
56: title: string
57: description: string
58: href: string
59: }
60:
61: interface ReviewMeta {
62: title: string // Full page title e.g. "Deputy Review 2026: Pricing, Features, Pros & Cons"
63: productName: string // e.g. "Deputy"
64: productUrl: string // e.g. "https://www.deputy.com"
65: productLogo?: string // e.g. "/images/logos/deputy.svg"
66: description: string // Meta description
67: excerpt: string // Short review summary for schema
68: author: string
69: authorImage: string
70: authorBio: string
71: authorUrl?: string
72: date: string // Published date in format: "22 December 2025"
73: dateISO: string // ISO 8601 format: "2025-12-22"
74: dateModified?: string // ISO 8601 format if updated
75: readTime: string // e.g. "19 min read"
76: overallScore: number // 1-10
77: categoryScores: CategoryScore[]
78: // For aggregate rating of reviewed product
79: aggregateRating?: {
80: ratingValue: string
81: ratingCount: string
82: reviewCount: string
83: }
84: // Product pricing for schema
85: startingPrice?: string // e.g. "9.75"
86: priceCurrency?: string // e.g. "AUD"
87: }
88:
89: interface CompareProduct {
90: name: string // e.g. "RosterElf"
91: logo: string // e.g. "/images/logos/rosterelf-logo-black.svg"
92: compareUrl: string // e.g. "/compare/deputy-vs-rosterelf"
93: compareTitle: string // e.g. "Honest Deputy vs RosterElf comparison 2026"
94: compareDescription: string
95: }
96:
97: interface Props {
98: reviewMeta: ReviewMeta
99: tocItems: TOCItem[]
100: faqCategories: FAQCategory[]
101: slug: string // e.g. "deputy" for /reviews/deputy
102: compareProduct?: CompareProduct
103: relatedResources?: RelatedResource[]
104: alternativeGuide?: {
105: title: string
106: description: string
107: href: string
108: }
109: }
110:
111: const { reviewMeta, tocItems, faqCategories, slug, compareProduct, relatedResources = [], alternativeGuide } = Astro.props
112:
113: // Generate FAQ schema
114: const faqSchema = {
115: '@context': 'https://schema.org',
116: '@type': 'FAQPage',
117: mainEntity: faqCategories.flatMap((category) =>
118: category.faqs.map((faq) => ({
119: '@type': 'Question',
120: name: faq.question,
121: acceptedAnswer: {
122: '@type': 'Answer',
123: text: faq.answer.replace(/<[^>]*>/g, ''),
124: },
125: }))
126: ),
127: }
128:
129: // Generate Review schema
130: const reviewSchema = {
131: '@context': 'https://schema.org',
132: '@type': 'Review',
133: name: reviewMeta.title,
134: reviewBody: reviewMeta.excerpt,
135: author: {
136: '@type': 'Person',
137: name: reviewMeta.author,
138: ...(reviewMeta.authorUrl && { url: `https://www.rosterelf.com${reviewMeta.authorUrl}` }),
139: },
140: datePublished: reviewMeta.dateISO,
141: ...(reviewMeta.dateModified && { dateModified: reviewMeta.dateModified }),
142: publisher: {
143: '@type': 'Organization',
144: name: 'RosterElf',
145: url: 'https://www.rosterelf.com',
146: logo: {
147: '@type': 'ImageObject',
148: url: 'https://www.rosterelf.com/images/logos/rosterelf-logo.svg',
149: },
150: },
151: reviewRating: {
152: '@type': 'Rating',
153: ratingValue: reviewMeta.overallScore.toString(),
154: bestRating: '10',
155: worstRating: '1',
156: },
157: itemReviewed: {
158: '@type': 'SoftwareApplication',
159: name: reviewMeta.productName,
160: applicationCategory: 'BusinessApplication',
161: operatingSystem: 'Web, iOS, Android',
162: description: reviewMeta.description,
163: url: reviewMeta.productUrl,
164: ...(reviewMeta.aggregateRating && {
165: aggregateRating: {
166: '@type': 'AggregateRating',
167: ratingValue: reviewMeta.aggregateRating.ratingValue,
168: bestRating: '5',
169: ratingCount: reviewMeta.aggregateRating.ratingCount,
170: reviewCount: reviewMeta.aggregateRating.reviewCount,
171: },
172: }),
173: ...(reviewMeta.startingPrice && {
174: offers: {
175: '@type': 'Offer',
176: price: reviewMeta.startingPrice,
177: priceCurrency: reviewMeta.priceCurrency || 'AUD',
178: priceValidUntil: `${new Date().getFullYear() + 1}-12-31`,
179: availability: 'https://schema.org/InStock',
180: },
181: }),
182: },
183: }
184:
185: // Default related resources if none provided
186: const defaultResources: RelatedResource[] = [
187: ...(alternativeGuide
188: ? [
189: {
190: tag: 'Switching guide',
191: title: alternativeGuide.title,
192: description: alternativeGuide.description,
193: href: alternativeGuide.href,
194: },
195: ]
196: : []),
197: ...(compareProduct
198: ? [
199: {
200: tag: 'Full comparison',
201: title: `${reviewMeta.productName} vs RosterElf`,
202: description: 'Detailed feature-by-feature comparison.',
203: href: compareProduct.compareUrl,
204: },
205: ]
206: : []),
207: {
208: tag: 'All reviews',
209: title: 'Software reviews',
210: description: 'Independent reviews of rostering platforms.',
211: href: '/reviews',
212: },
213: ]
214:
215: const finalResources = relatedResources.length > 0 ? relatedResources : defaultResources
216: ---
217:
218:
219:
227:
228:
229:
230:
231:
SOFTWARE REVIEWS
232:
233: Honest {reviewMeta.productName} review {new Date().getFullYear()}: Pricing, features, pros & cons
234:
235:
236: Updated {reviewMeta.date}
237: •
238: {reviewMeta.readTime}
239:
240:
241:
242:
243:
244:
245:
Written by
246:
{reviewMeta.author}
247:
248:
249:
259:
260:
261:
262:
263:
264:
265:
266:
267:
268:
269:
270:
271:
272: {
273: compareProduct && (
274:
289: )
290: }
291:
292:
293:
294:
295:
296: Review methodology
297:
298:
299:
300:
301:
312:
313:
314:
315:
316:
317:
318:
319:
320:
321:
322:
323:
324:
325:
````
## File: src/layouts/SupportArticleLayout.astro
````astro
1: ---
2: /**
3: * SupportArticleLayout - Standard layout for support/knowledge base articles
4: *
5: * HEADING STRUCTURE:
6: * - H1: Article title (provided by this layout from articleMeta.title)
7: * - H2: Main sections (e.g., "1. Enable shift notes")
8: * - H3: Subsections (e.g., "1.1 Turn on time and attendance")
9: * - H4: Not recommended - keep hierarchy to H1 → H2 → H3
10: *
11: * SCHEMAS INCLUDED:
12: * - HowTo schema (for step-by-step guides)
13: * - Article schema (general article metadata)
14: * - BreadcrumbList schema (navigation)
15: * - FAQPage schema (if FAQs provided)
16: * - VideoObject schema (if video provided)
17: */
18: import BaseLayout from './BaseLayout.astro'
19: import Breadcrumb from '../components/Breadcrumb.astro'
20: import TOCSidebar from '../components/TOCSidebar.astro'
21: import { ChevronRight, ChevronLeft, Shield, Monitor, Smartphone, Tablet, Clock, ArrowUp, Link, MessageCircle, Phone, Mail, Search } from 'lucide-astro'
22: import { getRelatedArticles, getArticleNavigation } from '../utils/supportArticleUtils'
23:
24: interface TOCItem {
25: label: string
26: href: string
27: }
28:
29: interface FAQ {
30: question: string
31: answer: string
32: }
33:
34: interface HowToStep {
35: name: string
36: text: string
37: image?: string
38: }
39:
40: interface Video {
41: name: string
42: description: string
43: videoId: string
44: uploadDate?: string // ISO 8601 date (e.g., "2024-01-15")
45: duration?: string // ISO 8601 duration (e.g., "PT5M" for 5 minutes)
46: }
47:
48: interface BreadcrumbItem {
49: label: string
50: href: string
51: }
52:
53: interface RelatedArticle {
54: title: string
55: href: string
56: category?: string
57: }
58:
59: interface ArticleNavigation {
60: previous?: { title: string; href: string }
61: next?: { title: string; href: string }
62: }
63:
64: interface ArticleMeta {
65: title: string
66: description: string
67: category: string
68: categorySlug: string
69: section?: string
70: sectionSlug?: string
71: permissionLevel: 'Admins' | 'Managers' | 'Staff' | 'All Users'
72: device: 'Web Browser' | 'Smartphone' | 'Tablet' | 'All Devices'
73: updatedDate: string // Human readable: "2 months ago" or "20 October 2025"
74: updatedDateISO: string // ISO format: "2025-10-20"
75: readTime?: string // e.g., "3 min read" - if not provided, will estimate
76: author?: {
77: name: string
78: slug: string
79: photo: string
80: }
81: }
82:
83: interface Props {
84: articleMeta: ArticleMeta
85: tocItems: TOCItem[]
86: faqs?: FAQ[]
87: howToSteps?: HowToStep[]
88: video?: Video
89: slug: string
90: relatedArticles?: RelatedArticle[]
91: navigation?: ArticleNavigation
92: }
93:
94: const { articleMeta, tocItems, faqs = [], howToSteps = [], video, slug, relatedArticles: manualRelatedArticles, navigation: manualNavigation } = Astro.props
95:
96: // Automatically generate related articles if not manually provided
97: const relatedArticles = manualRelatedArticles && manualRelatedArticles.length > 0 ? manualRelatedArticles : await getRelatedArticles(slug, articleMeta.categorySlug, articleMeta.device)
98:
99: // Automatically generate navigation if not manually provided
100: const navigation = manualNavigation ? manualNavigation : await getArticleNavigation(slug, articleMeta.categorySlug)
101:
102: // Estimate read time if not provided (roughly 200 words per minute)
103: const estimatedReadTime = articleMeta.readTime || '3 min read'
104:
105: // Build breadcrumb items
106: const breadcrumbItems: BreadcrumbItem[] = [
107: { label: 'Home', href: '/' },
108: { label: 'Support', href: '/support' },
109: { label: 'Knowledge base', href: '/support/knowledge-base' },
110: { label: articleMeta.category, href: `/support/knowledge-base/${articleMeta.categorySlug}` },
111: ]
112:
113: if (articleMeta.section && articleMeta.sectionSlug) {
114: breadcrumbItems.push({
115: label: articleMeta.section,
116: href: `/support/knowledge-base/${articleMeta.categorySlug}/${articleMeta.sectionSlug}`,
117: })
118: }
119:
120: breadcrumbItems.push({ label: articleMeta.title, href: `/support/knowledge-base/${articleMeta.categorySlug}/${slug}` })
121:
122: // Canonical URL
123: const canonicalUrl = `https://www.rosterelf.com/support/knowledge-base/${articleMeta.categorySlug}/${slug}`
124:
125: // Device icon mapping
126: const deviceIcons = {
127: 'Web Browser': Monitor,
128: Smartphone: Smartphone,
129: Tablet: Tablet,
130: 'All Devices': Monitor,
131: }
132: const DeviceIcon = deviceIcons[articleMeta.device] || Monitor
133:
134: // Publisher schema (RosterElf organization)
135: const publisherSchema = {
136: '@type': 'Organization',
137: name: 'RosterElf',
138: url: 'https://www.rosterelf.com',
139: logo: {
140: '@type': 'ImageObject',
141: url: 'https://www.rosterelf.com/logo.png',
142: width: 512,
143: height: 512,
144: },
145: }
146:
147: // BreadcrumbList schema
148: const breadcrumbSchema = {
149: '@context': 'https://schema.org',
150: '@type': 'BreadcrumbList',
151: itemListElement: breadcrumbItems.map((item, index) => ({
152: '@type': 'ListItem',
153: position: index + 1,
154: name: item.label,
155: item: item.href.startsWith('http') ? item.href : `https://www.rosterelf.com${item.href}`,
156: })),
157: }
158:
159: // Article schema
160: const articleSchema = {
161: '@context': 'https://schema.org',
162: '@type': 'TechArticle',
163: headline: articleMeta.title,
164: description: articleMeta.description,
165: dateModified: articleMeta.updatedDateISO,
166: publisher: publisherSchema,
167: ...(articleMeta.author && {
168: author: {
169: '@type': 'Person',
170: name: articleMeta.author.name,
171: url: `https://www.rosterelf.com/author/${articleMeta.author.slug}`,
172: image: `https://www.rosterelf.com${articleMeta.author.photo}`,
173: },
174: }),
175: mainEntityOfPage: {
176: '@type': 'WebPage',
177: '@id': canonicalUrl,
178: },
179: url: canonicalUrl,
180: articleSection: articleMeta.category,
181: inLanguage: 'en-AU',
182: isAccessibleForFree: true,
183: }
184:
185: // HowTo schema (if steps provided)
186: const howToSchema =
187: howToSteps.length > 0
188: ? {
189: '@context': 'https://schema.org',
190: '@type': 'HowTo',
191: name: articleMeta.title,
192: description: articleMeta.description,
193: step: howToSteps.map((step, index) => ({
194: '@type': 'HowToStep',
195: position: index + 1,
196: name: step.name,
197: text: step.text,
198: ...(step.image && {
199: image: {
200: '@type': 'ImageObject',
201: url: step.image.startsWith('http') ? step.image : `https://www.rosterelf.com${step.image}`,
202: },
203: }),
204: })),
205: }
206: : null
207:
208: // FAQ schema (if FAQs provided)
209: const faqSchema =
210: faqs.length > 0
211: ? {
212: '@context': 'https://schema.org',
213: '@type': 'FAQPage',
214: mainEntity: faqs.map((faq) => ({
215: '@type': 'Question',
216: name: faq.question,
217: acceptedAnswer: {
218: '@type': 'Answer',
219: text: faq.answer,
220: },
221: })),
222: }
223: : null
224:
225: // Video schema (if video provided)
226: const videoSchema = video
227: ? {
228: '@context': 'https://schema.org',
229: '@type': 'VideoObject',
230: name: video.name,
231: description: video.description,
232: thumbnailUrl: [
233: `https://img.youtube.com/vi/${video.videoId}/maxresdefault.jpg`,
234: `https://img.youtube.com/vi/${video.videoId}/sddefault.jpg`,
235: `https://img.youtube.com/vi/${video.videoId}/hqdefault.jpg`,
236: ],
237: uploadDate: video.uploadDate || '2024-01-01',
238: ...(video.duration && { duration: video.duration }),
239: contentUrl: `https://www.youtube.com/watch?v=${video.videoId}`,
240: embedUrl: `https://www.youtube.com/embed/${video.videoId}`,
241: publisher: publisherSchema,
242: }
243: : null
244: ---
245:
246:
247:
248:
249:
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
260:
261:
262:
Quick search
263:
264:
276:
277:
278:
279:
285:
286:
287: Press Enter to ask
288:
289:
290:
291:
292:
293:
294:
295:
296:
297:
298:
329:
330:
331:
332:
333:
334:
335: {
336: faqs.length > 0 && (
337: <>
338:
339: Frequently asked questions
340:
341:
342: {faqs.map((faq, index) => (
343:
344:
345: {index + 1}. {faq.question}
346:
347:
348:
349: ))}
350:
351: >
352: )
353: }
354:
355:
356:
357:
358:
359:
360:
361:
362:
Still need help?
363:
Our Australian-based support team is here to assist you.
364:
365:
380:
381:
382:
383:
384: {
385: navigation && (navigation.previous || navigation.next) && (
386:
413: )
414: }
415:
416:
417: {
418: relatedArticles && relatedArticles.length > 0 && (
419:
420:
421:
Related articles in {articleMeta.category}
422:
423:
433:
434: )
435: }
436:
437:
438:
439:
440:
441:
442:
443:
448:
449:
450:
451:
452:
453:
454:
455:
456:
457:
458:
459:
460:
461:
462:
463:
464:
465: {howToSchema && }
466: {faqSchema && }
467: {videoSchema && }
468:
469:
470:
578:
579:
580:
669:
````
## File: src/lib/sitemap-utils.ts
````typescript
1: import { readdir, readFile } from 'node:fs/promises'
2: import { join, relative } from 'node:path'
3: import { execSync } from 'node:child_process'
4:
5: // Use process.cwd() for reliable path resolution at build time
6: export const pagesDir = join(process.cwd(), 'src/pages')
7:
8: export interface SitemapUrl {
9: loc: string
10: lastmod?: string
11: changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'
12: priority?: number
13: }
14:
15: // Pages to exclude from sitemap (includes redirect pages)
16: const excludedPages = ['image-inventory', 'sitemap', 'pages', '404', 'dev/', 'signin', 'sign-in', 'example-premiere-page', '[slug]', '[category]', '[...', 'medical-centre', 'features/work-app']
17:
18: /**
19: * Extract premiere date from page content
20: * Returns the date string (YYYY-MM-DD) or null if not found
21: */
22: function getPremiereDate(content: string): string | null {
23: const premiereDateMatch = content.match(/premiereDate\s*=\s*['"]([^'"]+)['"]/)
24: if (!premiereDateMatch) return null
25: return premiereDateMatch[1]
26: }
27:
28: /**
29: * Check if a page has a future premiere date
30: */
31: function hasFuturePremiereDate(content: string): boolean {
32: const premiereDateStr = getPremiereDate(content)
33: if (!premiereDateStr) return false
34:
35: const premiereDate = new Date(premiereDateStr)
36: const now = new Date()
37: now.setHours(0, 0, 0, 0)
38: premiereDate.setHours(0, 0, 0, 0)
39:
40: return premiereDate > now
41: }
42:
43: /**
44: * Get last modified date from git or file stats
45: */
46: export function getLastModified(filePath: string): string {
47: try {
48: // Try to get the last commit date for this file from git
49: const gitDate = execSync(`git log -1 --format="%aI" -- "${filePath}"`, {
50: encoding: 'utf-8',
51: stdio: ['pipe', 'pipe', 'pipe'],
52: }).trim()
53:
54: if (gitDate) {
55: return gitDate.split('T')[0] // Return just the date part (YYYY-MM-DD)
56: }
57: } catch {
58: // Git command failed, fall back to file stats
59: }
60:
61: try {
62: // Fall back to file modification time (sync version for simplicity)
63: const stats = execSync(`stat -f "%Sm" -t "%Y-%m-%d" "${filePath}"`, {
64: encoding: 'utf-8',
65: stdio: ['pipe', 'pipe', 'pipe'],
66: }).trim()
67: return stats
68: } catch {
69: // Return today's date as last resort
70: return new Date().toISOString().split('T')[0]
71: }
72: }
73:
74: /**
75: * Get priority based on URL
76: */
77: export function getPriority(url: string): number {
78: if (url === '/') return 1.0
79: if (url === '/pricing' || url === '/contact' || url === '/features') return 0.9
80: if (url === '/ai-context' || url === '/ai-context.txt' || url === '/ai-info.json') return 0.8 // High priority for AI agents
81: if (url.startsWith('/features/')) return 0.8
82: if (url.startsWith('/industries/')) return 0.8
83: if (url.startsWith('/support/knowledge-base')) return 0.8
84: if (url.startsWith('/support')) return 0.7
85: if (url.startsWith('/reviews/') || url.startsWith('/alternatives/') || url.startsWith('/compare/')) return 0.7
86: if (url.startsWith('/guides/')) return 0.7
87: if (url.startsWith('/free-hr-templates/')) return 0.7
88: if (url.startsWith('/free-roster-templates/') || url.startsWith('/free-time-and-attendance-templates/') || url.startsWith('/free-award-and-payroll-templates/')) return 0.7
89: if (url.startsWith('/blog/') || url.startsWith('/insights/')) return 0.7
90: if (url.startsWith('/free-tools/')) return 0.7
91: if (url.startsWith('/why-rosterelf/')) return 0.6
92: if (url.startsWith('/job-descriptions/')) return 0.6
93: if (url.startsWith('/integrations/')) return 0.6
94: // UK pages: mirror root logic with slight priority reduction
95: if (url.startsWith('/uk/features/')) return 0.8
96: if (url.startsWith('/uk/industries/')) return 0.8
97: if (url.startsWith('/uk/blog/') || url.startsWith('/uk/insights/')) return 0.7
98: if (url.startsWith('/uk/guides/')) return 0.7
99: if (url.startsWith('/uk/')) return 0.7
100: return 0.5
101: }
102:
103: /**
104: * Get change frequency based on URL
105: */
106: export function getChangeFreq(url: string): SitemapUrl['changefreq'] {
107: if (url === '/' || url === '/pricing') return 'weekly'
108: if (url === '/ai-context' || url === '/ai-context.txt' || url === '/ai-info.json') return 'daily' // Updated on every build
109: if (url.startsWith('/blog/') || url.startsWith('/insights/')) return 'monthly'
110: if (url.startsWith('/guides/award-rates')) return 'monthly'
111: if (url.startsWith('/support/knowledge-base')) return 'monthly'
112: if (url.startsWith('/support')) return 'monthly'
113: if (url.startsWith('/features/') || url.startsWith('/industries/')) return 'monthly'
114: if (url.startsWith('/reviews/') || url.startsWith('/alternatives/')) return 'monthly'
115: if (url.startsWith('/product-updates/')) return 'monthly'
116: // UK pages: mirror root changefreq
117: if (url.startsWith('/uk/blog/') || url.startsWith('/uk/insights/')) return 'monthly'
118: if (url.startsWith('/uk/features/') || url.startsWith('/uk/industries/')) return 'monthly'
119: if (url.startsWith('/uk/guides/')) return 'monthly'
120: return 'yearly'
121: }
122:
123: /**
124: * Recursively scan pages directory with optional path filter
125: */
126: export async function scanPages(dir: string, baseDir: string, urls: SitemapUrl[] = [], pathFilter?: (path: string) => boolean): Promise {
127: try {
128: const entries = await readdir(dir, { withFileTypes: true })
129:
130: for (const entry of entries) {
131: if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue
132:
133: const fullPath = join(dir, entry.name)
134:
135: if (entry.isDirectory()) {
136: await scanPages(fullPath, baseDir, urls, pathFilter)
137: } else if (entry.isFile() && entry.name.endsWith('.astro')) {
138: const relativePath = relative(baseDir, fullPath)
139: const pathWithoutExtension = relativePath.replace(/\.astro$/, '')
140:
141: // Skip excluded pages
142: if (excludedPages.some((excluded) => pathWithoutExtension.includes(excluded))) continue
143:
144: // Convert file path to URL path
145: let url = '/' + pathWithoutExtension.replace(/\\/g, '/')
146: if (url.endsWith('/index')) {
147: url = url.replace(/\/index$/, '') || '/'
148: }
149:
150: // Apply path filter if provided
151: if (pathFilter && !pathFilter(url)) continue
152:
153: // Read file content
154: const content = await readFile(fullPath, 'utf-8')
155:
156: // Skip pages with future premiere dates
157: if (hasFuturePremiereDate(content)) continue
158:
159: // Get last modified date - use premiere date if present, otherwise git commit date
160: const premiereDate = getPremiereDate(content)
161: const lastmod = premiereDate || getLastModified(fullPath)
162:
163: urls.push({
164: loc: url,
165: lastmod,
166: changefreq: getChangeFreq(url),
167: priority: getPriority(url),
168: })
169: }
170: }
171: } catch (error) {
172: console.error('Error scanning pages:', error)
173: }
174:
175: return urls
176: }
177:
178: /**
179: * Generate sitemap XML from URLs
180: */
181: export function generateSitemapXML(urls: SitemapUrl[], baseUrl: string): string {
182: const urlsXML = urls
183: .map((url) => {
184: const loc = `${baseUrl}${url.loc}`
185: const lastmod = url.lastmod ? ` ${url.lastmod} ` : ''
186: const changefreq = url.changefreq ? ` ${url.changefreq} ` : ''
187: const priority = url.priority ? ` ${url.priority.toFixed(1)} ` : ''
188:
189: return `
190: ${loc} ${lastmod ? '\n' + lastmod : ''}${changefreq ? '\n' + changefreq : ''}${priority ? '\n' + priority : ''}
191: `
192: })
193: .join('\n')
194:
195: return `
196:
197: ${urlsXML}
198: `
199: }
200:
201: /**
202: * Generate sitemap index XML
203: */
204: export function generateSitemapIndexXML(sitemaps: string[], baseUrl: string): string {
205: const today = new Date().toISOString().split('T')[0]
206: const sitemapsXML = sitemaps
207: .map(
208: (sitemap) => `
209: ${baseUrl}/${sitemap}
210: ${today}
211: `
212: )
213: .join('\n')
214:
215: return `
216:
217: ${sitemapsXML}
218: `
219: }
````
## File: astro.config.mjs
````javascript
1: // @ts-check
2: import { defineConfig } from 'astro/config'
3:
4: import tailwindcss from '@tailwindcss/vite'
5: import { redirects } from './src/data/redirects.js'
6:
7: // https://astro.build/config
8: export default defineConfig({
9: site: 'https://www.rosterelf.com',
10: output: 'static',
11: trailingSlash: 'never',
12: image: {
13: // Enable Sharp for image optimization
14: service: {
15: entrypoint: 'astro/assets/services/sharp',
16: config: {
17: limitInputPixels: false,
18: },
19: },
20: },
21: vite: {
22: plugins: [tailwindcss()],
23: build: {
24: minify: 'esbuild',
25: cssMinify: true,
26: },
27: },
28: redirects,
29: })
````