Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Set date
document.getElementById('dateStamp').textContent = new Date().toLocaleDateString('en-GB', {
day:'2-digit', month:'short', year:'numeric'
}).toUpperCase();
// ISSN format on input
document.getElementById('issnInput').addEventListener('input', function(e) {
let v = e.target.value.replace(/[^0-9Xx]/g, '');
if (v.length > 4) v = v.slice(0,4) + '-' + v.slice(4,8);
e.target.value = v;
});
document.getElementById('issnInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') search();
});
function fillAndSearch(issn) {
document.getElementById('issnInput').value = issn;
search();
}
function showLoader(v) {
document.getElementById('loader').style.display = v ? 'block' : 'none';
}
function showError(msg) {
document.getElementById('error').style.display = msg ? 'block' : 'none';
document.getElementById('errorMsg').textContent = msg || '';
}
function clearResults() {
document.getElementById('results').innerHTML = '';
}
async function search() {
const raw = document.getElementById('issnInput').value.trim();
const issn = raw.replace(/[^0-9Xx-]/g,'').toUpperCase();
if (!/^\d{4}-[\dX]{4}$/i.test(issn)) {
showError('⚠ Invalid ISSN format. Please enter in the format XXXX-XXXX (e.g. 0028-0836).');
clearResults();
return;
}
showError('');
clearResults();
showLoader(true);
try {
const [crossrefData, openAlexData, articlesData] = await Promise.allSettled([
fetchCrossRef(issn),
fetchOpenAlex(issn),
fetchRecentArticles(issn)
]);
showLoader(false);
const cr = crossrefData.status === 'fulfilled' ? crossrefData.value : null;
const oa = openAlexData.status === 'fulfilled' ? openAlexData.value : null;
const arts = articlesData.status === 'fulfilled' ? articlesData.value : [];
if (!cr && !oa) {
showError(`✗ No journal found for ISSN ${issn}. Please verify the number and try again.`);
return;
}
renderResults(issn, cr, oa, arts);
} catch(e) {
showLoader(false);
showError('⚠ Network error: ' + e.message);
}
}
async function fetchCrossRef(issn) {
const res = await fetch(`https://api.crossref.org/journals/${issn}`);
if (!res.ok) return null;
const j = await res.json();
return j.message || null;
}
async function fetchOpenAlex(issn) {
const res = await fetch(`https://api.openalex.org/sources?filter=issn:${issn}&per_page=1`);
if (!res.ok) return null;
const j = await res.json();
return j.results && j.results.length > 0 ? j.results[0] : null;
}
async function fetchRecentArticles(issn) {
const res = await fetch(`https://api.crossref.org/journals/${issn}/works?rows=8&sort=published&order=desc`);
if (!res.ok) return [];
const j = await res.json();
return (j.message && j.message.items) ? j.message.items : [];
}
function val(v, fallback = '—') {
if (v === undefined || v === null || v === '') return fallback;
return v;
}
function renderResults(issn, cr, oa, articles) {
const container = document.getElementById('results');
let html = '';
// ── Journal metadata card ──
const title = (cr && cr.title) || (oa && oa.display_name) || issn;
const publisher = (cr && cr.publisher) || (oa && oa.host_organization_name) || '—';
const subjects = (cr && cr.subjects && cr.subjects.length)
? cr.subjects.map(s => s.name).join(', ')
: (oa && oa.topics && oa.topics.length ? oa.topics.slice(0,4).map(t=>t.display_name).join(', ') : '—');
const homepage = (cr && cr['URL']) || (oa && oa.homepage_url) || null;
const totalWorks = (cr && cr.counts && cr.counts['total-dois'])
? cr.counts['total-dois'].toLocaleString()
: (oa && oa.works_count ? oa.works_count.toLocaleString() : '—');
const citedByCount = (oa && oa.cited_by_count) ? oa.cited_by_count.toLocaleString() : '—';
const isOpenAccess = (oa && oa.is_oa) ? 'Yes' : (oa ? 'No' : '—');
const country = (oa && oa.country_code) ? oa.country_code : '—';
const type = (oa && oa.type) ? oa.type.replace(/-/g,' ') : (cr && cr.type ? cr.type : '—');
// ISSN badges
let issnBadges = `<span class="issn-badge">${issn}</span>`;
if (cr && cr.ISSN && cr.ISSN.length > 1) {
cr.ISSN.forEach((i,idx) => {
if (i !== issn) issnBadges += `<span class="issn-badge electronic">${i}</span>`;
});
}
if (cr && cr['issn-type']) {
issnBadges = '';
cr['issn-type'].forEach(it => {
const cls = it.type === 'print' ? 'print' : 'electronic';
issnBadges += `<span class="issn-badge ${cls}" title="${it.type}">${it.value} <small style="opacity:.6;font-size:.9em">${it.type.charAt(0).toUpperCase()}</small></span>`;
});
}
// Sources used
const sources = [];
if (cr) sources.push('CrossRef');
if (oa) sources.push('OpenAlex');
html += `
<div class="journal-card">
<div class="card-header">
<div class="journal-title">${escHtml(title)}</div>
<div style="margin-top:.6rem">${issnBadges}</div>
</div>
<div class="card-body">
<div class="info-item">
<div class="info-key">Publisher</div>
<div class="info-val">${escHtml(publisher)}</div>
</div>
<div class="info-item">
<div class="info-key">Type</div>
<div class="info-val" style="text-transform:capitalize">${escHtml(type)}</div>
</div>
<div class="info-item">
<div class="info-key">Country</div>
<div class="info-val">${escHtml(country)}</div>
</div>
<div class="info-item">
<div class="info-key">Open Access</div>
<div class="info-val">${isOpenAccess}</div>
</div>
<div class="info-item">
<div class="info-key">Total Works</div>
<div class="info-val">${totalWorks}</div>
</div>
<div class="info-item">
<div class="info-key">Cited By</div>
<div class="info-val">${citedByCount}</div>
</div>
${subjects !== '—' ? `
<div class="info-item" style="grid-column:1/-1">
<div class="info-key">Subject Areas</div>
<div class="info-val">${escHtml(subjects)}</div>
</div>` : ''}
${homepage ? `
<div class="info-item" style="grid-column:1/-1">
<div class="info-key">Homepage</div>
<div class="info-val"><a href="${escHtml(homepage)}" target="_blank" rel="noopener">${escHtml(homepage)}</a></div>
</div>` : ''}
</div>
<div class="source-strip">
<span class="source-label">Data from:</span>
${sources.map(s=>`<span class="source-tag">${s}</span>`).join('')}
</div>
</div>`;
// ── Recent Articles ──
if (articles && articles.length > 0) {
html += `<div class="section-divider"><span>Recent Articles (CrossRef)</span></div>`;
html += `<div class="articles-section">
<div class="articles-header">
<span>Latest Publications</span>
<span class="articles-count">${articles.length} results</span>
</div>`;
articles.forEach(art => {
const artTitle = (art.title && art.title.length) ? art.title[0] : 'Untitled';
const authors = (art.author && art.author.length)
? art.author.slice(0,3).map(a => a.family ? `${a.given ? a.given[0]+'. ' : ''}${a.family}` : '').filter(Boolean).join(', ') + (art.author.length > 3 ? ' et al.' : '')
: '';
const year = art.published && art.published['date-parts'] && art.published['date-parts'][0]
? art.published['date-parts'][0][0]
: (art.created && art.created['date-parts'] ? art.created['date-parts'][0][0] : '');
const doi = art.DOI || '';
const volume = art.volume ? `Vol. ${art.volume}` : '';
const issue = art.issue ? `Issue ${art.issue}` : '';
const pages = art.page ? `pp. ${art.page}` : '';
const meta = [volume, issue, pages].filter(Boolean).join(' · ');
html += `<div class="article-row">
<div class="article-doi-row">
<div class="article-title">${escHtml(artTitle)}</div>
${year ? `<div class="article-year">${year}</div>` : ''}
</div>
<div class="article-meta">
${authors ? `<span>${escHtml(authors)}</span>` : ''}
${meta ? `<span style="margin:0 .4rem;opacity:.5">·</span><span>${escHtml(meta)}</span>` : ''}
${doi ? `<span style="margin:0 .4rem;opacity:.5">·</span><a href="https://doi.org/${doi}" target="_blank" rel="noopener">DOI: ${escHtml(doi)}</a>` : ''}
</div>
</div>`;
});
html += `</div>`;
}
container.innerHTML = html;
}
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"');
}