- Introduced a new 'shops' category in the build script and README. - Implemented badge filtering in the filter functionality, allowing users to filter entries by badge.
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
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 `<span class="badge badge-tier-${badge.tier}" title="${badge.description}">${badge.label}</span>`;
|
|
}
|
|
|
|
// 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
|
|
? `<div class="entry-image"><img src="${entry.image}" alt="${entry.title}" loading="lazy"></div>`
|
|
: '';
|
|
|
|
const logoHtml = entry.logo
|
|
? `<img src="${entry.logo}" alt="${entry.title} logo" class="entry-logo">`
|
|
: '';
|
|
|
|
const badgeHtml = getBadgeHTML(entry.badge);
|
|
|
|
return `
|
|
<article class="entry-card"
|
|
data-category="${entry.category || ''}"
|
|
data-region="${entry.region || ''}"
|
|
data-tags="${(entry.tags || []).join(',')}"
|
|
data-badge="${entry.badge || ''}">
|
|
${imageHtml}
|
|
<div class="entry-card-content">
|
|
<div class="entry-header">
|
|
${logoHtml}
|
|
<h2><a href="${entry.url}">${entry.title}</a></h2>
|
|
${badgeHtml}
|
|
</div>
|
|
<div class="entry-meta">
|
|
<button type="button" class="filter-btn category" data-filter-type="category" data-filter-value="${entry.category || ''}">${translateCategory(entry.category) || ''}</button>
|
|
${entry.region ? `<button type="button" class="filter-btn region" data-filter-type="region" data-filter-value="${entry.region}">${entry.region}</button>` : ''}
|
|
</div>
|
|
<div class="entry-body">${entry.body}</div>
|
|
${entry.tags?.length ? `
|
|
<div class="tags">
|
|
${entry.tags.map(tag => `<button type="button" class="tag filter-btn" data-filter-type="tag" data-filter-value="${tag}">${tag}</button>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</article>
|
|
`}).join('\n');
|
|
}
|
|
|
|
// Generate single entry page HTML
|
|
function generateEntryPage(entry, template) {
|
|
const imageHtml = entry.image
|
|
? `<div class="single-image"><img src="${entry.image}" alt="${entry.title}"></div>`
|
|
: '';
|
|
|
|
const logoHtml = entry.logo
|
|
? `<img src="${entry.logo}" alt="${entry.title} logo" class="single-logo">`
|
|
: '';
|
|
|
|
const badgeHtml = getBadgeHTML(entry.badge);
|
|
|
|
const entryHtml = `
|
|
<article class="single-entry">
|
|
${imageHtml}
|
|
<div class="single-header">
|
|
${logoHtml}
|
|
<div class="single-title-wrapper">
|
|
<h1>${entry.title}</h1>
|
|
${badgeHtml}
|
|
</div>
|
|
</div>
|
|
<div class="single-meta">
|
|
<a href="../index.html?category=${encodeURIComponent(entry.category || '')}" class="filter-link category">${translateCategory(entry.category) || ''}</a>
|
|
${entry.region ? `<a href="../index.html?region=${encodeURIComponent(entry.region)}" class="filter-link region">${entry.region}</a>` : ''}
|
|
</div>
|
|
<div class="single-body">${entry.body}</div>
|
|
${entry.website ? `<a href="${entry.website}" class="website-link" target="_blank" rel="noopener">Besök webbplats →</a>` : ''}
|
|
${entry.tags?.length ? `
|
|
<div class="tags">
|
|
${entry.tags.map(tag => `<a href="../index.html?tag=${encodeURIComponent(tag)}" class="tag">${tag}</a>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
<a href="../index.html" class="back-link">← Tillbaka till alla poster</a>
|
|
</article>
|
|
`;
|
|
|
|
return renderTemplate(template, {
|
|
title: `${entry.title} | Ursprung Sverige`,
|
|
subtitle: entry.title,
|
|
content: entryHtml,
|
|
year: new Date().getFullYear()
|
|
});
|
|
}
|
|
|
|
// 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 =>
|
|
`<option value="${c}">${translateCategory(c)}</option>`
|
|
).join('');
|
|
|
|
const regionOptions = filters.regions.map(r =>
|
|
`<option value="${r}">${r}</option>`
|
|
).join('');
|
|
|
|
const tagOptions = filters.tags.map(t =>
|
|
`<option value="${t}">${t}</option>`
|
|
).join('');
|
|
|
|
const badgeOptions = filters.badges.map(b =>
|
|
`<option value="${b}">${BADGE_TIERS[b]?.label || b}</option>`
|
|
).join('');
|
|
|
|
return `
|
|
<div class="filters">
|
|
<div class="filter-group">
|
|
<label for="category-filter">Kategori</label>
|
|
<select id="category-filter">
|
|
<option value="">Alla kategorier</option>
|
|
${categoryOptions}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="region-filter">Region</label>
|
|
<select id="region-filter">
|
|
<option value="">Alla regioner</option>
|
|
${regionOptions}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="badge-filter">Märkning</label>
|
|
<select id="badge-filter">
|
|
<option value="">Alla märkningar</option>
|
|
${badgeOptions}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="tag-filter">Tagg</label>
|
|
<select id="tag-filter">
|
|
<option value="">Alla taggar</option>
|
|
${tagOptions}
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="search-input">Sök</label>
|
|
<input type="text" id="search-input" placeholder="Sök...">
|
|
</div>
|
|
<button type="button" id="clear-filters" class="clear-filters-btn">Rensa filter</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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()
|
|
});
|
|
|
|
// Write index.html
|
|
fs.writeFileSync(path.join(DIST_DIR, 'index.html'), indexHtml);
|
|
|
|
// Generate badges info page
|
|
const badgesTemplatePath = path.join(TEMPLATE_DIR, 'badges.html');
|
|
if (fs.existsSync(badgesTemplatePath)) {
|
|
const badgesTemplate = fs.readFileSync(badgesTemplatePath, 'utf-8');
|
|
const badgesHtml = renderTemplate(badgesTemplate, {
|
|
year: new Date().getFullYear()
|
|
});
|
|
fs.writeFileSync(path.join(DIST_DIR, 'om-markning.html'), badgesHtml);
|
|
}
|
|
|
|
// Generate about page
|
|
const aboutTemplatePath = path.join(TEMPLATE_DIR, 'about.html');
|
|
if (fs.existsSync(aboutTemplatePath)) {
|
|
const aboutTemplate = fs.readFileSync(aboutTemplatePath, 'utf-8');
|
|
const aboutHtml = renderTemplate(aboutTemplate, {
|
|
year: new Date().getFullYear()
|
|
});
|
|
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();
|
|
}
|