740 gradientStart,
741 gradientEnd,
742 logoImageSrc,
743 textColor,
744 logoColor,
763
764 // Create a cache key based on the input parameters
765 const cacheKey = `og:${link}:${title}:${subtitle}:${gradientStart}:${gradientEnd}:${logoImageSrc}:${textColor}:${logoColor}:${titleFontSize}:${subtitleFontSize}:${urlFontSize}:${showLogoName}`;
766
767 // Try to get the cached image from Val Town blob
768 if (typeof nocache == 'undefined' || nocache !== 'true') {
769 try {
770 const cachedImage = await blob.get(cacheKey);
771 if (cachedImage) {
772 // Check if cachedImage is a Response object
773 if (cachedImage instanceof Response) {
774 // Ensure the response is OK
775 if (cachedImage.ok) {
776 const imageBuffer = await cachedImage.arrayBuffer();
777 c.header("Content-Type", "image/png");
778 c.header("X-Cache", "HIT");
779 console.log("Cache hit for", cacheKey);
780 return c.body(new Uint8Array(imageBuffer));
781 }
782 } else {
783 // If it's already a Uint8Array, use it directly
784 c.header("Content-Type", "image/png");
785 c.header("X-Cache", "HIT");
786 console.log("Cache hit for", cacheKey);
787 return c.body(cachedImage);
788 }
789 } else {
791 }
792 } catch (error) {
793 console.error("Error fetching image from Val Town blob:", error);
794 }
795 }
796 // If we reach here, either there was no cache hit or nocache was true
797 // Continue with generating a new image...
798
799 // Use provided title and subtitle, or fallback to defaults
800 const imageTitle = title || "";
801 const imageSubtitle = subtitle || "";
802 // Define colors
803 // gray: 161413
825
826
827 // const logoImageSrc = 'https://labspace.ai/ls2-circle-md4x.png';
828 logoImageSrc = logoImageSrc || 'https://labspace.ai/lsc-blue-transparent4x.png';
829
830 const svg = await satori({
887 WebkitBoxOrient: 'vertical',
888 },
889 children: imageTitle,
890 },
891 },
902 marginBottom: '0px',
903 },
904 children: imageSubtitle,
905 },
906 },
979 // type: 'img',
980 // props: {
981 // src: logoImageSrc,
982 // alt: 'Labspace Logo',
983 // style: {
984 // width: '72px',
985 // objectFit: 'contain',
986 // marginBottom: '0px', // Add space between image and text
987 // marginRight: '0px',
988 // },
1146 const pngBuffer = await render(svg);
1147
1148 // Cache the generated image in Val Town blob
1149 try {
1150 // Check if the pngBuffer size is less than 128KB (Val Town blob limit)
1153 c.header("X-Cache", "MISS");
1154 } else {
1155 console.warn("Image too large to cache in Val Town blob");
1156 c.header("X-Cache", "SKIP");
1157 }
1158 } catch (error) {
1159 console.error("Error caching image in Val Town blob:", error);
1160 c.header("X-Cache", "ERROR");
1161 }
1162
1163 c.header("Content-Type", "image/png");
1164 return c.body(pngBuffer);
1165});
1174 gradientStart,
1175 gradientEnd,
1176 logoImageSrc,
1177 textColor,
1178 logoColor,
1202
1203 // Create a cache key based on the input parameters
1204 const cacheKey = `og:${link}:${title}:${subtitle}:${attachment}:${objectFit}:${objectPosition}:${panelWidth}:${width}:${height}:${gradientStart}:${gradientEnd}:${logoImageSrc}:${textColor}:${logoColor}:${titleFontSize}:${subtitleFontSize}:${urlFontSize}:${showLogoName}`;
1205
1206 // Try to get the cached image from Val Town blob
1207 if (typeof nocache == 'undefined' || nocache !== 'true') {
1208 try {
1209 const cachedImage = await blob.get(cacheKey);
1210 if (cachedImage) {
1211 // Check if cachedImage is a Response object
1212 if (cachedImage instanceof Response) {
1213 // Ensure the response is OK
1214 if (cachedImage.ok) {
1215 const imageBuffer = await cachedImage.arrayBuffer();
1216 c.header("Content-Type", "image/png");
1217 c.header("X-Cache", "HIT");
1218 console.log("Cache hit for", cacheKey);
1219 return c.body(new Uint8Array(imageBuffer));
1220 }
1221 } else {
1222 // If it's already a Uint8Array, use it directly
1223 c.header("Content-Type", "image/png");
1224 c.header("X-Cache", "HIT");
1225 console.log("Cache hit for", cacheKey);
1226 return c.body(cachedImage);
1227 }
1228 } else {
1230 }
1231 } catch (error) {
1232 console.error("Error fetching image from Val Town blob:", error);
1233 }
1234 }
1235 // If we reach here, either there was no cache hit or nocache was true
1236 // Continue with generating a new image...
1237
1238 // Use provided title and subtitle, or fallback to defaults
1239 const imageTitle = title || "";
1240 const imageSubtitle = subtitle || "";
1241
1242 // Define colors
1258 // }
1259
1260 // const logoImageSrc = 'https://labspace.ai/ls2-circle-md4x.png';
1261 logoImageSrc = logoImageSrc || 'https://labspace.ai/lsc-blue-transparent4x.png';
1262
1263 const svg = await satori({
1294 flexDirection: 'column',
1295 maxWidth: panelWidth, // panelWidth, // Control width of text section
1296 paddingRight: '48px', // Space between text and image
1297
1298 paddingTop: '14px',
1330 WebkitBoxOrient: 'vertical',
1331 },
1332 children: imageTitle,
1333 },
1334 },
1345 marginBottom: '0px',
1346 },
1347 children: imageSubtitle,
1348 },
1349 },
1355 },
1356 attachment && {
1357 // Right panel with image
1358 type: 'div',
1359 props: {
1457 // type: 'img',
1458 // props: {
1459 // src: logoImageSrc,
1460 // alt: 'Labspace Logo',
1461 // style: {
1462 // width: '72px',
1463 // objectFit: 'contain',
1464 // marginBottom: '0px', // Add space between image and text
1465 // marginRight: '0px',
1466 // },
1624 const pngBuffer = await render(svg);
1625
1626 // Cache the generated image in Val Town blob
1627 try {
1628 // Check if the pngBuffer size is less than 128KB (Val Town blob limit)
1631 c.header("X-Cache", "MISS");
1632 } else {
1633 console.warn("Image too large to cache in Val Town blob");
1634 c.header("X-Cache", "SKIP");
1635 }
1636 } catch (error) {
1637 console.error("Error caching image in Val Town blob:", error);
1638 c.header("X-Cache", "ERROR");
1639 }
1640
1641 c.header("Content-Type", "image/png");
1642 return c.body(pngBuffer);
1643});
1656 <meta charset="UTF-8">
1657 <meta name="viewport" content="width=device-width, initial-scale=1.0">
1658 <link rel="icon" type="image/png" href="https://labspace.ai/ls2-circle.png" />
1659 <title>OG Image Generator</title>
1660 <meta property="og:title" content="OG Image Generator" />
1661 <meta property="og:description" content="Generate custom Open Graph images for your web pages." />
1662 <meta property="og:image" content="https://yawnxyz-og.web.val.run/img2?link=https://yawnxyz-og.web.val.run&title=OG+Image+Generator&subtitle=Generate+custom+Open+Graph+images+for+your+web+pages.&attachment=https://f2.phage.directory/blogalog/og-labspace-ai.png" />
1663 <meta property="og:url" content="https://yawnxyz-og.web.val.run/" />
1664 <meta property="og:type" content="website" />
1665 <meta name="twitter:card" content="summary_large_image" />
1666 <meta name="twitter:title" content="OG Image Generator" />
1667 <meta name="twitter:description" content="Generate custom Open Graph images for your web pages." />
1668 <meta name="twitter:image" content="https://yawnxyz-og.web.val.run/img2?link=https://yawnxyz-og.web.val.run&title=OG+Image+Generator&subtitle=Generate+custom+Open+Graph+images+for+your+web+pages.&attachment=https://f2.phage.directory/blogalog/og-labspace-ai.png" />
1669 <script src="https://cdn.tailwindcss.com"></script>
1670 <script src="https://unpkg.com/dexie@3.2.2/dist/dexie.js"></script>
1734 <div class="AppContainer">
1735 <div class="App">
1736 <h1>OG Image Generator</h1>
1737 <p class="pb-4">Generate custom Open Graph images for your web pages.</p>
1738 <form id="ogForm" class="mb-4">
1739 <!-- <label class="text-xs text-gray-500" for="title">Title</label> -->
1743 <!-- <label class="text-xs text-gray-500" for="link">Page URL</label> -->
1744 <input type="text" id="link" placeholder="Enter page URL" class="mb-2" value="${link||''}">
1745 <!-- <label class="text-xs text-gray-500" for="attachment">Image Attachment URL (optional)</label> -->
1746 <input type="text" id="attachment" placeholder="Enter image attachment URL (optional)" class="mb-2" value="${attachment||''}">
1747 <div id="attachmentOptions" class="hidden">
1748 <label class="text-xs text-gray-500" for="objectFit">Object Fit</label>
1758 <label class="text-xs text-gray-500" for="panelWidth">Panel Width</label>
1759 <input type="text" id="panelWidth" placeholder="Panel Width (e.g., 30%)" class="mb-2">
1760 <label class="text-xs text-gray-500" for="width">Image Width</label>
1761 <input type="text" id="width" placeholder="Image Width (e.g., 1200)" class="mb-2">
1762 <label class="text-xs text-gray-500" for="height">Image Height</label>
1763 <input type="text" id="height" placeholder="Image Height (e.g., 630)" class="mb-2">
1764 </div>
1765 <button type="submit" class="bg-button text-white px-4 py-2 rounded hover:bg-button-hover transition-colors">
1766 Generate OG Image
1767 </button>
1768 <span id="timerDisplay" class="ml-2 text-sm"></span>
1769 </form>
1770 <div id="imageList" class="mb-4 space-y-2"></div>
1771 <div class="relative">
1772 <div id="loadingPlaceholder" class="hidden w-full h-64 rounded-md mb-4 bg-accent border border-app-accent flex items-center justify-center">
1773 <div class="animate-spin rounded-full h-12 w-12 border-4 border-button border-t-transparent"></div>
1774 </div>
1775 <img id="ogImage" src="" alt="Generated OG Image" class="w-full rounded-md mb-4 hidden">
1776 <button id="downloadButton"
1777 class="absolute top-2 right-2 bg-button text-white px-2 py-1 rounded text-sm hover:bg-button-hover transition-colors hidden">
1779 </button>
1780 </div>
1781 <div id="imageUrlContainer" class="hidden relative mb-4 break-all bg-accent border border-app-accent rounded-md p-2">
1782 <span id="imageUrl" class="text-sm"></span>
1783 <button id="copyButton"
1784 class="absolute top-[6px] right-1 bg-button text-white px-2 py-1 rounded text-sm hover:bg-button-hover transition-colors hidden">
1789 <div class="flex flex-col items-left">
1790 <div class="text-sm">Width</div>
1791 <span id="imageWidth" class="counter">0</span>
1792 </div>
1793 <div class="flex flex-col items-left">
1794 <div class="text-sm">Height</div>
1795 <span id="imageHeight" class="counter">0</span>
1796 </div>
1797 <div class="flex flex-col items-left">
1798 <div class="text-sm">Size</div>
1799 <span id="imageSize" class="counter">0 KB</span>
1800 </div>
1801 </div>
1813 <script>
1814 // Initialize Dexie
1815 const db = new Dexie('OGImageGenerator');
1816 db.version(1).stores({
1817 images: '++id, link, title, subtitle, imageUrl'
1818 });
1819
1820 const form = document.getElementById('ogForm');
1821 const ogImage = document.getElementById('ogImage');
1822 const downloadButton = document.getElementById('downloadButton');
1823 const imageWidth = document.getElementById('imageWidth');
1824 const imageHeight = document.getElementById('imageHeight');
1825 const imageSize = document.getElementById('imageSize');
1826 const imageUrl = document.getElementById('imageUrl');
1827 const imageUrlContainer = document.getElementById('imageUrlContainer');
1828 const copyButton = document.getElementById('copyButton');
1829 const imageList = document.getElementById('imageList');
1830 const timerDisplay = document.getElementById('timerDisplay');
1831 const attachmentInput = document.getElementById('attachment');
1852 }
1853
1854 async function updateImageList() {
1855 const images = await db.images.reverse().limit(50).toArray();
1856 imageList.innerHTML = images.map(image =>
1857 '<div class="image-item flex flex-row justify-between items-center p-2 bg-accent border border-app-accent rounded-md cursor-pointer hover:bg-app-bg-alt transition-colors" data-id="' + image.id + '">' +
1858 '<span>' + image.title + '</span>' +
1859 '<button class="delete-button self-end bg-button text-white px-2 py-1 rounded text-sm hover:bg-button-hover transition-colors">' +
1860 'Delete' +
1863 ).join('');
1864 }
1865 imageList.addEventListener('click', async (e) => {
1866 if (e.target.classList.contains('image-item') || e.target.tagName === 'SPAN') {
1867 const imageId = parseInt(e.target.closest('.image-item').dataset.id);
1868 const image = await db.images.get(imageId);
1869 // Remove bg-white from all items
1870 document.querySelectorAll('.image-item').forEach(item => {
1871 item.classList.remove('bg-white');
1872 });
1873 // Add bg-white to selected item
1874 e.target.closest('.image-item').classList.add('bg-white');
1875
1876 if (image) {
1877 displayImageData(image);
1878 }
1879 } else if (e.target.classList.contains('delete-button')) {
1880 e.stopPropagation();
1881 const imageId = parseInt(e.target.closest('.image-item').dataset.id);
1882 if (confirm('Are you sure you want to delete this image?')) {
1883 await db.images.delete(imageId);
1884 await updateImageList();
1885 }
1886 }
1887 });
1888
1889 function displayImageData(image) {
1890 document.getElementById('link').value = image.link;
1891 document.getElementById('title').value = image.title;
1892 document.getElementById('subtitle').value = image.subtitle;
1893 document.getElementById('attachment').value = image.attachment || '';
1894
1895 // Show loading placeholder and hide image
1896 loadingPlaceholder.classList.remove('hidden');
1897 ogImage.classList.add('hidden');
1898
1899 // Set up the image load sequence
1900 ogImage.onload = () => {
1901 // Only hide placeholder after image is fully loaded and rendered
1902 loadingPlaceholder.classList.add('hidden');
1903 ogImage.classList.remove('hidden');
1904 downloadButton.classList.remove('hidden');
1905 };
1906
1907 // Start loading the image
1908 ogImage.src = image.imageUrl;
1909
1910 imageUrl.textContent = image.imageUrl;
1911 imageUrlContainer.classList.remove('hidden');
1912 copyButton.classList.remove('hidden');
1913 updateImageStats(image.imageUrl);
1914 }
1915
1916 async function updateImageStats(url) {
1917 const response = await fetch(url);
1918 const blob = await response.blob();
1919 const img = new Image();
1920 img.onload = function() {
1921 imageWidth.textContent = this.width;
1922 imageHeight.textContent = this.height;
1923 imageSize.textContent = (blob.size / 1024).toFixed(2) + ' KB';
1924 }
1925 img.src = URL.createObjectURL(blob);
1950 params.append('objectPosition', document.getElementById('objectPosition').value);
1951 }
1952 const imageUrlValue = attachment ? '/img2?' + params.toString() : '/img?' + params.toString();
1953
1954 startTimer();
1955 timerDisplay.classList.remove('text-green-600');
1956
1957 // Show loading placeholder and hide image/button
1958 loadingPlaceholder.classList.remove('hidden');
1959 ogImage.classList.add('hidden');
1960 downloadButton.classList.add('hidden');
1961
1962 try {
1963 const response = await fetch(imageUrlValue + '&nocache=true');
1964 const blob = await response.blob();
1965 const url = URL.createObjectURL(blob);
1966
1967 // Set up the image load sequence
1968 ogImage.onload = () => {
1969 // Only hide placeholder after image is fully loaded and rendered
1970 loadingPlaceholder.classList.add('hidden');
1971 ogImage.classList.remove('hidden');
1972 downloadButton.classList.remove('hidden');
1973 };
1974
1975 // Start loading the image
1976 ogImage.src = url;
1977
1978 imageUrl.textContent = window.location.origin + imageUrlValue;
1979 imageUrlContainer.classList.remove('hidden');
1980 copyButton.classList.remove('hidden');
1981
1982 updateImageStats(url);
1983
1984 // Save to Dexie
1985 await db.images.add({
1986 link,
1987 title,
1992 height: document.getElementById('height').value,
1993 objectPosition: document.getElementById('objectPosition').value,
1994 imageUrl: window.location.origin + imageUrlValue
1995 });
1996
1997 // Update image list
1998 await updateImageList();
1999 } catch (error) {
2000 loadingPlaceholder.classList.add('hidden');
2001 console.error('Error generating image:', error);
2002 alert('Failed to generate image. Please try again.');
2003 } finally {
2004 stopTimer();
2008 downloadButton.addEventListener('click', () => {
2009 const link = document.createElement('a');
2010 link.href = ogImage.src;
2011 link.download = 'og-image.png';
2012 link.click();
2013 });
2014
2015 copyButton.addEventListener('click', () => {
2016 const urlText = imageUrl.textContent;
2017 navigator.clipboard.writeText(urlText).then(() => {
2018 // alert('URL copied to clipboard!');
2028 document.getElementById('objectPosition').value = 'center';
2029
2030 // Initial load of image list
2031 updateImageList();
2032 </script>
2033</body>