import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { marked } from 'marked';
const CONTENT_DIR = './content';
const TEMPLATE_DIR = './templates';
const PUBLIC_DIR = './public';
const STATIC_DIR = './static';
const DIST_DIR = './dist';
// Swedish translations for categories
const CATEGORY_LABELS = {
manufacturers: 'Tillverkare',
products: 'Produkter',
services: 'Tjänster',
experiences: 'Upplevelser',
shops: 'Butiker'
};
// Badge tiers (highest to lowest)
const BADGE_TIERS = {
// Tier 1 - Gold (universal)
'akta-svenskt': { label: 'Äkta Svenskt', tier: 1, description: 'Helt svenskt: företag, produktion/drift och leverantörer' },
// Tier 2 - Silver (physical or digital)
'tillverkat-i-sverige': { label: 'Tillverkat i Sverige', tier: 2, description: 'Svenskt företag, tillverkat i Sverige' },
'svenskt-moln': { label: 'Svenskt Moln', tier: 2, description: 'Svenskt företag med svenska servrar och hosting' },
// Tier 3 - Bronze (physical or digital)
'designat-i-sverige': { label: 'Designat i Sverige', tier: 3, description: 'Svensk design, produktion kan ske utomlands' },
'utvecklat-i-sverige': { label: 'Utvecklat i Sverige', tier: 3, description: 'Svensk utveckling, hosting kan ske utomlands' },
// Tier 4 - Blue (universal)
'svenskt-foretag': { label: 'Svenskt Företag', tier: 4, description: 'Svenskregistrerat eller svenskgrundat företag' }
};
function translateCategory(category) {
return CATEGORY_LABELS[category] || category;
}
function getBadgeHTML(badgeKey) {
if (!badgeKey || !BADGE_TIERS[badgeKey]) return '';
const badge = BADGE_TIERS[badgeKey];
return `${badge.label}`;
}
// Ensure directory exists
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
// Copy directory recursively
function copyDirRecursive(src, dest) {
const items = fs.readdirSync(src, { withFileTypes: true });
for (const item of items) {
const srcPath = path.join(src, item.name);
const destPath = path.join(dest, item.name);
if (item.isDirectory()) {
ensureDir(destPath);
copyDirRecursive(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
// Read all markdown files from a directory recursively
function readContentFiles(dir) {
const entries = [];
if (!fs.existsSync(dir)) {
return entries;
}
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
entries.push(...readContentFiles(fullPath));
} else if (item.name.endsWith('.md')) {
const content = fs.readFileSync(fullPath, 'utf-8');
const { data, content: body } = matter(content);
const category = path.basename(path.dirname(fullPath));
const slug = path.basename(item.name, '.md');
entries.push({
...data,
category: data.category || category,
body: marked(body),
bodyRaw: body,
slug,
url: `${category}/${slug}.html`,
sourcePath: fullPath
});
}
}
return entries;
}
// Simple template replacement
function renderTemplate(template, data) {
let result = template;
for (const [key, value] of Object.entries(data)) {
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
result = result.replace(regex, value ?? '');
}
return result;
}
// Generate entry cards HTML
function generateEntryCards(entries) {
return entries.map(entry => {
const imageHtml = entry.image
? `

`
: '';
const logoHtml = entry.logo
? `
`
: '';
const badgeHtml = getBadgeHTML(entry.badge);
return `
${imageHtml}
${entry.region ? `` : ''}
${entry.body}
${entry.tags?.length ? `
${entry.tags.map(tag => ``).join('')}
` : ''}
`}).join('\n');
}
// Generate single entry page HTML
function generateEntryPage(entry, template) {
const imageHtml = entry.image
? ``
: '';
const logoHtml = entry.logo
? `
`
: '';
const badgeHtml = getBadgeHTML(entry.badge);
const entryHtml = `
${imageHtml}
${entry.body}
${entry.website ? `Besök webbplats →` : ''}
${entry.tags?.length ? `
` : ''}
← Tillbaka till alla poster
`;
return renderTemplate(template, {
title: `${entry.title} | Ursprung Sverige`,
subtitle: entry.title,
content: entryHtml,
year: new Date().getFullYear(),
footer: getFooter('../')
});
}
// Extract unique values for filters
function extractFilters(entries) {
const categories = [...new Set(entries.map(e => e.category).filter(Boolean))].sort();
const regions = [...new Set(entries.map(e => e.region).filter(Boolean))].sort();
const tags = [...new Set(entries.flatMap(e => e.tags || []))].sort();
const badges = [...new Set(entries.map(e => e.badge).filter(Boolean))].sort((a, b) => {
// Sort by tier (highest first)
const tierA = BADGE_TIERS[a]?.tier || 99;
const tierB = BADGE_TIERS[b]?.tier || 99;
return tierA - tierB;
});
return { categories, regions, tags, badges };
}
// Generate filter HTML
function generateFilterHTML(filters) {
const categoryOptions = filters.categories.map(c =>
``
).join('');
const regionOptions = filters.regions.map(r =>
``
).join('');
const tagOptions = filters.tags.map(t =>
``
).join('');
const badgeOptions = filters.badges.map(b =>
``
).join('');
return `
`;
}
// Load footer partial
function getFooter(rootPath = '') {
const footerPath = path.join(TEMPLATE_DIR, 'partials', 'footer.html');
if (!fs.existsSync(footerPath)) {
return '';
}
const footerTemplate = fs.readFileSync(footerPath, 'utf-8');
return renderTemplate(footerTemplate, {
year: new Date().getFullYear(),
rootPath
});
}
// Main build function
function build() {
console.log('Building site...');
// Clean and create dist directory
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true });
}
ensureDir(DIST_DIR);
// Read content
const entries = readContentFiles(CONTENT_DIR);
console.log(`Found ${entries.length} entries`);
// Extract filters
const filters = extractFilters(entries);
// Read templates
const indexTemplatePath = path.join(TEMPLATE_DIR, 'template.html');
const singleTemplatePath = path.join(TEMPLATE_DIR, 'single.html');
if (!fs.existsSync(indexTemplatePath)) {
console.error('Template not found:', indexTemplatePath);
process.exit(1);
}
const indexTemplate = fs.readFileSync(indexTemplatePath, 'utf-8');
const singleTemplate = fs.existsSync(singleTemplatePath)
? fs.readFileSync(singleTemplatePath, 'utf-8')
: indexTemplate; // Fallback to index template
// Generate index HTML
const indexHtml = renderTemplate(indexTemplate, {
title: 'Ursprung Sverige',
subtitle: 'En kurerad samling av svenska produkter, tjänster, upplevelser och tillverkare',
filters: generateFilterHTML(filters),
entries: generateEntryCards(entries),
entryCount: entries.length,
year: new Date().getFullYear(),
footer: getFooter('')
});
// Write index.html
fs.writeFileSync(path.join(DIST_DIR, 'index.html'), indexHtml);
// Generate badges info page
const badgesTemplatePath = path.join(TEMPLATE_DIR, 'om-markning.html');
if (fs.existsSync(badgesTemplatePath)) {
const badgesTemplate = fs.readFileSync(badgesTemplatePath, 'utf-8');
const badgesHtml = renderTemplate(badgesTemplate, {
year: new Date().getFullYear(),
footer: getFooter('')
});
fs.writeFileSync(path.join(DIST_DIR, 'om-markning.html'), badgesHtml);
}
// Generate about page
const aboutTemplatePath = path.join(TEMPLATE_DIR, 'om-oss.html');
if (fs.existsSync(aboutTemplatePath)) {
const aboutTemplate = fs.readFileSync(aboutTemplatePath, 'utf-8');
const aboutHtml = renderTemplate(aboutTemplate, {
year: new Date().getFullYear(),
footer: getFooter('')
});
fs.writeFileSync(path.join(DIST_DIR, 'om-oss.html'), aboutHtml);
}
// Generate individual entry pages
for (const entry of entries) {
const categoryDir = path.join(DIST_DIR, entry.category);
ensureDir(categoryDir);
const entryHtml = generateEntryPage(entry, singleTemplate);
fs.writeFileSync(path.join(categoryDir, `${entry.slug}.html`), entryHtml);
}
console.log(`Generated ${entries.length} entry pages`);
// Copy public assets (CSS, JS - small files)
if (fs.existsSync(PUBLIC_DIR)) {
copyDirRecursive(PUBLIC_DIR, DIST_DIR);
}
// Symlink static folder (images, large assets - avoids copying)
const staticDest = path.join(DIST_DIR, 'static');
if (fs.existsSync(STATIC_DIR) && !fs.existsSync(staticDest)) {
const absoluteStatic = path.resolve(STATIC_DIR);
fs.symlinkSync(absoluteStatic, staticDest);
}
console.log('Build complete! Output in', DIST_DIR);
}
// Watch mode
if (process.argv.includes('--watch')) {
console.log('Watching for changes...');
build();
const watchDirs = [CONTENT_DIR, TEMPLATE_DIR, PUBLIC_DIR];
for (const dir of watchDirs) {
if (fs.existsSync(dir)) {
fs.watch(dir, { recursive: true }, (event, filename) => {
console.log(`\nChange detected: ${filename}`);
build();
});
}
}
} else {
build();
}