Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
152353 stories
·
33 followers

Putting the Westwind.Scripting Templating Library to work, Part 2

1 Share

Part2 Banner

This is a two part series that discusses the Westwind.Scripting Template library

In part 1 of this series I introduced the Westwind.Scripting library, how it works and how you can use it and integrate it into your applications. In part 2, I'm going over implementation details of hosting a scripting engine for a specific scenario that generates local static Html output for a for a project based solution that requires both local preview and local Web site generation. As part of that process I'll point out a number of issues that you likely have to consider in this somewhat common scenario.

If you haven't already, I'd recommend you read Part 1 so you have a good idea what the library provides and how it works. While not required, this post will make a lot more sense with that context.

Putting Templating to use in a Real World Scenario

While using the ScriptParser for demos and single template situations is easy enough, using it to integrate into a larger application and interacting with host application features and content, especially generating Web site output, requires a bit more work.

I'm using my Documentation Monster application as an example here. It's a project based documentation solution that statically produces Html output that generates Html output in two ways in an offline desktop application:

  • Renders a single topic for Live Preview as you type topic content
  • Renders many topics in a documentation project into a full self-contained Web site

DM uses script templates to render each topic, with a specific topic type - Topic, Header, ClassHeader, ClassMethod, ClassProperty, WhatsNew, ExternalLink etc. - each representing a separate Html template in an Html file (ie. Topic.html) on disk which are the templates I'm passing into the ScriptParser class.

These topic templates templates have overlapping content: All of them have a header and topic body, but many of the topic types have customized areas to them: For example, ClassHeader has a class member table, inheritance list, lists assembly and namespace, ClassProperty/ClassMethod/ClassField member templates have syntax and exception settings, ExternalLink displays an Html page during development but redirects to a Url in a published output file etc. In other words, each topic type has some unique things going on with it that the template script reflects. If a topic has a type that can't map to a template, the default template which in this case is the Topic template is used via fallback.

Templates are user customizable, so they are sensitive to changes and are recompiled whenever changes are detected in the generated code.

The raw Html rendering of topics is simple enough - the template is executed as is and produces Html output. But but once you introduce document dependencies like images, scripts, css etc. and you create output that may end up in nested folders, pathing becomes a concern in statically generated content. The reason is, the 'hosting' environment can't be determined at render time and the content may be hosted locally via file system (for individual preview in this case), a root Web site, or in a sub-folder of a Web site.

So then the question is what's a relative path based on? What's a root path (/) based on? The rendered output has to be self-contained, and templates are responsible for properly making paths natural to use for a specific output environment.

This means some or all Urls may have to be fixed up and in some cases a <base> has to be provided in the Html content for each page. None of that can happen automatically so this is a manual post-processing step that is application specific.

In addition, when rendering topics a few things that need to be considered:

  • When and how to render using the ScriptParser
  • Where to store the Templates consistently
  • Ensure that rendered output can be referenced relatively
  • Ensure there's consistent BasePath to reference root and project wide Urls (ie. / or ~/)

Let's walk through what template rendering looks like inside of an application.

Create a TemplateHost

In projects that use template rendering I like to create a TemplateHost class that encapsulates all tasks related to executing the scripting engine. This simplifies configuration of the template engine in one place and provides a few easily accessible methods for rendering templates - in the case of DM rendering topics to string and to file.

ScriptParser Configuration - References and Namespaces

Adding of dependent references and namespaces is one of the most frustrating things of doing runtime compilation of code and so that step needs to be consolidated into a single place.

It's very likely when you start with scripts or code compilation you'll miss some dependencies, so the CreateScriptParser() method implementation is likely to be a work in process especially when you start out.

It's important to have an error mechanism in place that can capture the error messages that tell you which dependencies may be required explicitly.

Here's the base implementation of the TemplateHost with the CreateScriptParser() method implementation from DM:

public class TemplateHost
{
    public ScriptParser Script
    {
        get
        {
            if (field == null)
                field = CreateScriptParser();
            return field;
        }
        set;
    }

    public static ScriptParser CreateScriptParser()
    {
        var script = new ScriptParser();            
        
        // Good chunk of .NET Default libs
        script.ScriptEngine.AddDefaultReferencesAndNamespaces();
        
        // Any library in Host app that's been loaded up to this point
        // In DM everything required actually is loaded through this
        script.ScriptEngine.AddLoadedReferences();
        
        // explicit assemblies not or not yet used by host
        //script.ScriptEngine.AddAssembly("privatebin/Westwind.Ai.dll");
        //script.ScriptEngine.AddAssembly(typeof(Westwind.Utilities.StringUtils));

        script.ScriptEngine.AddNamespace("Westwind.Utilities");
        script.ScriptEngine.AddNamespace("DocMonster");
        script.ScriptEngine.AddNamespace("DocMonster.Model");            
        script.ScriptEngine.AddNamespace("DocMonster.Templates");
        script.ScriptEngine.AddNamespace("MarkdownMonster");
        script.ScriptEngine.AddNamespace("MarkdownMonster.Utilities");

        script.ScriptEngine.SaveGeneratedCode = true;
        script.ScriptEngine.CompileWithDebug = true;            

        // {{ expr }} is Html encoded - {{! expr }} required for raw Html output
        script.ScriptingDelimiters.HtmlEncodeExpressionsByDefault = true;            

		// custom props that expose in the template without Model. prefix
        script.AdditionalMethodHeaderCode =
            """
            DocTopic Topic = Model.Topic;
            var Project = Model.Project;
            var Configuration = Model.Configuration;
            var Helpers = Model.Helpers;                
            var DocMonsterModel = Model.DocMonsterModel;
            var AppModel = MarkdownMonster.mmApp.Model;
            
            var BasePath = new Uri(FileUtils.NormalizePath( Project.ProjectDirectory + "\\") );

            """;            
        return script;
    }
    
    // render methods below
}

In DM the TemplateHost is created on first access of a Project.TemplateHost property and then persists for the lifetime of the project unless explicitly recreated as there is some overhead in creating the script parser environment and we might be generating a lot of documents very quickly.

Notice that the parser is set up with common default and all loaded assembly references from the host. I then add all the specific libraries that may not have been loaded yet, and any custom namespaces that are used by the various application specific components that are used in the templates. This is perhaps the main reason to use a TemplateHost like wrapper: To hide away all this application specific configuration for a one time config and then can be forgotten about - you don't want to be doing this sort of thing in your application or business logic code.

Another thing of note: The AdditionalMethodHeaderCode property is used to expose various objects as top level objects to the template script. So rather than having to specify {{ Model.Topic.Title }} we can j ust use {{ Topic.Title }} and {{ Helpers.ChildTopicsList() }} for example. Shortcuts are useful, and you can stuff anything you want to expose in the script beyond the model here.

Since the templates in DM are accessible to end-users for editing, making the template expressions simpler makes for a more user friendly experience. Highly recommended.

Rendering

The actual render code is pretty straight forward by calling RenderTemplateFile() which renders a template from file:

public string RenderTemplateFile(string templateFile, RenderTemplateModel model)
{
    ErrorMessage = null;

    Script.ScriptEngine.ObjectInstance = null; // make sure we don't cache
    
    // explicitly turn these off for live output
	Script.ScriptEngine.SaveGeneratedCode = false;
	Script.ScriptEngine.CompileWithDebug = false;
	Script.ScriptEngine.DisableAssemblyCaching = false;
    
    string basePath = model.Project.ProjectDirectory;
    model.PageBasePath = System.IO.Path.GetDirectoryName(model.Topic.RenderTopicFilename);

    string result = Script.ExecuteScriptFile(templateFile, model, basePath: basePath);

    if (Script.Error)
	{
	    // run again this time with debugging options on
	    Script.ScriptEngine.SaveGeneratedCode = true;
	    Script.ScriptEngine.CompileWithDebug = true;
	    Script.ScriptEngine.DisableAssemblyCaching = true;  // force a recompile
	
	    Script.ExecuteScriptFile(templateFile, model, basePath: basePath);
	
	    Script.ScriptEngine.SaveGeneratedCode = false;
	    Script.ScriptEngine.CompileWithDebug = false;
	    Script.ScriptEngine.DisableAssemblyCaching = false;
	
	    // render the error page
	    result = ErrorHtml(model);
	    ErrorMessage = Script.ErrorMessage + "\n\n" + Script.GeneratedClassCodeWithLineNumbers;
	}

    return result;
}

This is the basic raw template execution logic that produces direct generated output - in this case Html.

Note that the processing checks for template errors which captures either compilation or runtime errors. If an error occurs, the current render process is re-run with all the debug options turned on so I can get additional error information to display on the error page.

I'll talk more about the error display in a minute.

Template Layout

I haven't talked about what the templates look like: DM uses relatively small topic templates, with a more complex Layout page that provides for the Web site's page chrome. The actual project output renders both the content the headers and footers and there's a bunch of logic to pull in the table of contents and handle navigation to new topics efficiently. The preview renders the same content but some of the aspects like the table of content are visually hidden in that mode.

All of that logic is encapsulated in the layout page and the supporting JavaScript scripts.

At the core re the Html/Handlebars topic templates. As mentioned, each topic type is a template that is rendered. Topic, Header, ExternalLink, WhatsNew, ClassHeader, ClassProperty, ClassMethod etc. each with their own custom formats. Each of the templates then references the same layout page (you could have several different one however if you chose)

Here's an example Content Page:

topic.html Template

{{%
    Script.Layout = "_layout.html";
}}

<h2 class="content-title">
    <img src="/_docmonster/icons/{{ Model.Topic.DisplayType }}.png">
    {{ Model.Topic.Title }}
</h2>

<div class="content-body" id="body">
    {{% if (Topic.IsLink && Topic.Body.Trim().StartsWith("http")) { }}
        <ul>
        <li>
            <a href="{{! Model.Topic.Body }}" target="_blank">{{ Model.Topic.Title }}</a>
            <a href="{{! Model.Topic.Body }}" target="_blank"><i class="fa-solid fa-up-right-from-square" style="font-size: 0.7em; vertical-align: super;"></i></a>
        </li>
        </ul>

        <blockquote style="font-size: 0.8em;"><i>In rendered output this link opens in a new browser window.
            For preview purposes, the link is displayed in this generic page.
            You can click the link to open the browser with the link which is the behavior you see when rendered.</i>
        </blockquote>
    {{% } else { }}
        {{ Model.Helpers.Markdown(Model.Topic.Body) }}
    {{% } }}
    
</div>

{{% if (!string.IsNullOrEmpty(Model.Topic.Remarks)) {  }}
    <h3 class="outdent" id="remarks">Remarks</h3>
    {{ Helpers.Markdown(ModelTopic.Remarks) }}
{{% } }}


{{% if (!string.IsNullOrEmpty(Topic.Example))  {  }}
    <h3 class="outdent" id="example">Example</h3>
    {{ Helpers.Markdown(Topic.Example) }}
{{% } }}

{{% if (!string.IsNullOrEmpty(Topic.SeeAlso)) { }}
    <h4 class="outdent" id="seealso">See also</h4>
    <div class="see-also-container">
        {{ Helpers.FixupSeeAlsoLinks(Topic.SeeAlso) }}
    </div>
{{% } }}

For demonstration purposes I'm showing both the Model.Topic and the custom header based direct binding to Topic via script.AdditionalMethodHeaderCode I showed earlier. Both point at the same value.

Note also the code block at the top that pulls in the Layout page:

{{%
    Script.Layout = "_layout.html";
}}

The layout page then looks like this:

_Layout.html

<!DOCTYPE html>
<html>
<head>
    {{%
     var theme = Project.Settings.RenderTheme;
     if(Topic.TopicState.IsPreview) { }}
    <base href="{{ Model.PageBasePath }}" />
    {{% } }}

    <meta charset="utf-8" />
    <title>{{ Topic.Title }} - {{ Project.Title }}</title>

    {{% if (!string.IsNullOrEmpty(Topic.Keywords)) { }}
    <meta name="keywords" content="{{ Topic.Keywords.Replace(" \n",", ") }}" />
    {{% } }}
    {{% if(!string.IsNullOrEmpty(Topic.Abstract)) { }}
    <meta name="description" content="{{! Topic.Abstract }}" />
    {{% } }}
    <meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1" />
    <link rel="stylesheet" type="text/css" href="~/_docmonster/themes/scripts/bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="~/_docmonster/themes/scripts/fontawesome/css/font-awesome.min.css" />
    <link id="AppCss" rel="stylesheet" type="text/css" href="~/_docmonster/themes/{{ theme }}/docmonster.css" />

    <script src="~/_docmonster/themes/scripts/highlightjs/highlight.pack.js"></script>
    <script src="~/_docmonster/themes/scripts/highlightjs-badge.min.js"></script>
    <link href="~/_docmonster/themes/scripts/highlightjs/styles/vs2015.css" rel="stylesheet" />
    <script src="~/_docmonster/themes/scripts/bootstrap/bootstrap.bundle.min.js" async></script>
    <script src="~/_docmonster/themes/scripts/lunr/lunr.min.js"></script>
    <script>
        window.page = {};
        window.page.basePath = "{{ Project.Settings.RelativeBaseUrl }}";
        window.renderTheme="{{ Project.Settings.RenderThemeMode }}";
    </script>
    <script src="~/_docmonster/themes/scripts/docmonster.js"></script>

    {{% if(Topic.TopicState.IsPreview) { }}
    <!-- Preview Navigation and Syncing -->
    <script src="~/_docmonster/themes/scripts/preview.js"></script>
    {{% } }}

</head>
<body>
    <!-- Markdown Monster Content -->
    <div class="flex-master">
        <div class="banner">
            <div class="float-end">
                <button id="themeToggleBtn" type="button" onclick="toggleTheme()"
                        class="btn btn-sm btn-secondary theme-toggle"
                        title="Toggle Light/Dark Theme">
                    <i id="themeToggleIcon"
                       class="fa fa-moon text-warning">
                    </i>
                </button>
            </div>

            <div class="float-start sidebar-toggle">
                <i class="fa fa-bars"
                   title="Show or hide the topics list"></i>
            </div>

			{{% if (Topic.Incomplete) { }}
               <div class="float-end mt-2 " title="This topic is under construction.">
                   <i class="fa-duotone fa-triangle-person-digging fa-lg fa-beat"
                   style="--fa-primary-color: #333; --fa-secondary-color: goldenrod; --fa-secondary-opacity: 1; --fa-animation-duration: 3s;"></i>
	           </div>
		    {{% } }}

            <img src="~/images/logo.png" class="banner-logo" />
            <div class="projectname"> {{ Project.Title }}</div>

            <div class="byline">
                <img src="~/_docmonster/icons/{{ Topic.DisplayType }}.png">
                {{ Topic.Title }}
            </div>
        </div>
        <div class="page-content">
            <div id="toc-container" class="sidebar-left toc-content">
                <nav class="visually-hidden">
                    <a href="~/tableofcontents.html">Table of Contents</a>
                </nav>
            </div>

            <div class="splitter">
            </div>

            <nav class="topic-outline">
                <div class="topic-outline-header">On this page:</div>
                <div class="topic-outline-content"></div>
            </nav>

            <div id="MainContent" class="main-content">
                <!-- Rendered Content -->

                <article class="content-pane">
                    {{ Script.RenderContent() }}
                </article>

                <div class="footer">

                    <div class="float-start">
                        &copy; {{ Project.Owner }}, {{ DateTime.Now.Year }} &bull;
                        updated: {{ Topic.Updated.ToString("MMM dd, yyyy") }}
                        <br />
                        {{%
                            string mailBody = $"Project: {Project.Title}\nTopic: {Topic.Title}\n\nUrl:\n{ Project.Settings.WebSiteBaseUrl?.TrimEnd('/') + Project.Settings.RelativeBaseUrl }{ Topic.Id }.html";
                            mailBody = WebUtility.UrlEncode(mailBody).Replace("+", "%20");
                        }}
                        <a href="mailto:{{ Project.Settings.SupportEmail }}?subject=Support: {{ Project.Title }} - {{ Topic.Title }}&body={{ mailBody }}">Comment or report problem with topic</a>
                    </div>

                    <div class="float-end">
                        <a href="https://documentationmonster.com" target="_blank"><img src="~/_docmonster/images/docmonster.png" style="height: 3.8em"/></a>
                    </div>
                </div>
                <!-- End Rendered Content -->
            </div> <!-- End MainContent -->
        </div> <!-- End page-content -->
    </div>   <!-- End flex-master -->
    
    <!-- End Markdown Monster Content -->
</body>
</html>

The full rendered site output looks something like this for a topic:

Documentation Monster Rendered Html Site
Figure 1 - Documentation Monster Rendered site output

The rendered topic content is in the middle panel of the display - all the rest is both static and dynamically rendered Html that is controlled through the Layout page. Both the table of contents and the document outline on the right are rendered using dynamic loading via JavaScript, while the header and footer are static with some minor embedded expressions.

With a Layout page this is easy to set up and maintain as there's a single page that handles that logic and it's easily referenced from each of the topic templates with a single layout page reference.

Behind the scenes, the parser looks for a Layout page directive in the content page requested by the ScriptParser, and if it finds one combines the layout page and content page into a single page that is executed. Essentially the Script.Layout command in the content page causes the referenced Layout page to be loaded, and the {{ Script.RenderContent() }} tag then is replaced with the content page content resulting in a single Html/Handlebars template. This combined content is then compiled and executed to produce the final merged output.

So far things are pretty straight forward relying on the core features of the scripting engine: We're pointing at template and a layout page and it produces Html in various forms depending on the type of template that we are dealing with.

That still leaves the task fixing up paths to make sure they work in the final 'hosting' environment.

Base Path Handling - PageBaseFolder and BaseFolder

When rendering Html output that depends on other resources like images CSS and scripts that referenced either as relative or site rooted paths, it's important that the page context can find these resources based on natural relative and absolute page path resolution.

There are two concerns:

  • Page Base Path for page relative links
  • Project Base Path for site root paths

Page Base Path for Relative Linking

Page base path refers to resolving relative paths in the document. For example from the current page referencing an image as SomeImage.png (same folder) or ../images/SomeFolder (relative path). In order for these relative paths to work the page has to be either running directly from the folder or the page has to be mapped into that page context.

In the context of a Web site that's simple as you have a natural page path that always applies. However, for previewing pages that are often rendered to a temporary file in an external location, which is then displayed in a WebView for preview. In that scenario relative path needs to be fixed up so it can find resolve links.

This scenario can be handled by explicitly forcing the page's <base> path to reflect the current page's path:

{{%
    if(Topic.TopicState.IsPreview)  { }}
       <base href="{{ Model.PageBasePath }}" />
{{%  }  }}

Note that I'm only applying the <base> tag in Preview mode. In Preview the Html is rendered into a temp location, so I map back to the actual location where relative content is expected and that makes relative links work.

Here's what that looks like:

Page Base Path And Root Path
Figure 2 - Providing a Page Base path when rendering to a temp location is crucial to ensure relative resources like images can be found!

Without the <base> path in the document, the page would look for the image in the rendered output location - in the TMP folder - and of course would not find the image there.

For final rendered output running in a Web Browser, this is not necessary as the page naturally runs out of the appropriate folder and no <base> path is applied.

Why not just render local output into the 'correct relative location' where relative content can be found?
For local preview that's often impractical due to permissions or simply for cluttering up folders with temporary render files. Generated files can wreak havoc with source control or Dropbox and permissions unless explicit exceptions are set up. I recommend that local WebView content that has dependencies should always be rendered to a temporary location and then be back-linked via <base> paths to ensure that relative paths can resolve.

Root Paths In Temporary Location

The other more important issue has to do with resolving the root path. This is especially important when rendering to a temporary file, but it can even be an issue if you create a 'site' that sits off a Web root.

For example, most of my documentation 'sites' live in a /docs folder off the main Web site rather than at the / root.

For example:

In this site, any links that reference / and intend to go back to the documentation root, are going to jump back to the main Web site root instead. From a project perspective I want / to mean the project root, but I don't want to have to figure out while I'm working on the content whether I have to use /docs/ or /. In fact, I never want to hard code a reference to /docs/ in my templates or in user content, but expect to reference the project root as /.

In DM this is very relevant when the site is generated. We can then specify a root folder / by default or /docs/ explicitly as shown here entered for my specific site:

Providing Page Base Path When Rendering
Figure 3 - When rendering to a non-root location it has to be generated at Html generation time.

Here's what this looks like in the template (you can use either ~/ or /):

<link id="AppCss" rel="stylesheet" type="text/css" 
      href="~/_docmonster/themes/{{ theme }}/docmonster.css" />

and here is the rendered output with the /docs/ path for any / or ~/ starting Urls:

<link id="AppCss" rel="stylesheet" type="text/css" 
      href="/docs/_docmonster/themes/Dharkan/docmonster.css" />

This fixup applies to any Url generated both from the templates and from user generated topic content so the fixup has to occur post rendering - it's not something that can be fixed via the template.

This fixup also happens in Preview mode where the full path is prefixed which produces these nasty looking paths:

<link id="AppCss" rel="stylesheet" type="text/css" 
      href="C:/Users/rstrahl/Documents/Documentation Monster/MarkdownMonster/_docmonster/themes/Dharkan/docmonster.css" />

This means that the root path need to be fixed up depending on the root path environment. There are several scenarios:

  • Root is root / rendered into root of site for final Html Site output - no replacements required.
  • Root is a subfolder (ie. /docs/) - / is replaced with /docs/ in all paths
  • Root is the project folder from Temp location - / is replaced with a folder file location or WebView virtual host name root

This is something that is not part of the ScriptParser class, but rather has to be handled at the application layer post fix up, and it's fairly specific to the Html based generation that takes place. In my template based applications I always have do one form or another of this sort of fix up.

Here's what this looks like in DM:

string html = Project.TemplateHost.RenderTemplateFile(templateFile, model);

...

// Fix up any locally linked .md extensions to .html
string basePath = null;
if (renderMode == TopicRenderModes.Html)
{
 	 // fix up DM specific `dm-XXX` links like topic refs, .md file links etc.
     html = FixupHtmlLinks(html, renderMode);  

     // Specified value in dialog ('/docs/` or `/`)
     basePath = Project.Settings.RelativeBaseUrl;  // 
}            
else if (renderMode == TopicRenderModes.Preview || renderMode == TopicRenderModes.Chm)
{
	// special `dm-XXX` links are handled via click handlers
	
	// Project directory is our base folder
    basePath = Path.TrimEndingDirectorySeparator(Project.ProjectDirectory).Replace("\\", "/") + "/";
}

html = html
           .Replace("=\"/", "=\"" + basePath)
           .Replace("=\"~/", "=\"" + basePath)
           // UrlEncoded
           .Replace("=\"%7E/", "=\"" + basePath)
           // Escaped
          .Replace("=\"\\/", "=\"/")
          .Replace("=\"~\\/", "=\"/");
          
if(renderMode == TopicRenderModes.Preview ||
   renderMode == TopicRenderModes.Print || 
   renderMode == TopicRenderModes.Chm)
{
   // explicitly specify local page path
   var path = Path.GetDirectoryName(topic.GetExternalFilename()).Replace("\\","/") + "/";
   html = html.Replace("=\"./", "=\"" + path)
              .Replace("=\"../", "=\"" + path + "../");
}

OnAfterRender(html, renderMode);

return html;          

The key piece is the Html fix up block that takes any / or ~/ links - including some variations - and explicitly replaces the actual base path that's specified.

Another issue is addressed in the following block that deals with print and local pages: Relative links don't resolve in print and PDF output, so they have to be explicitly specfied as part of the link. Apparently the WebView print engine ignores <basePath> for many things and so even relative links have to be fixed up with an explicit prefix even if the page is in the correct location.

So yeah - path handling is a pain in the ass! 🤣 But hopefully this section gives you the information you need to fix up your paths as needed.

Error Handling

When a template is run there is a possibility that it can fail. Typically it fails because there's an error in the template itself, which tends to generate compilation errors, or you can also run into runtime errors when code execution fails.

The ScriptParser automatically captures error information for both compilation and runtime errors are provides convenient members to access them. There's ErrorMessage and `` If an error occurs, DM checks for it and then generates an error page:

if (Script.Error)
{
    result = ErrorHtml();
    ErrorMessage = Script.ErrorMessage + "\n\n" + Script.GeneratedClassCodeWithLineNumbers;
}

where ErrorHtml() is the method that creates the error.

You can keep this real simple and just render the error message and optionally the code:

public string ErrorHtml(string errorMessage = null, string code = null)
{            
    if (string.IsNullOrEmpty(errorMessage))
        errorMessage = Script.ErrorMessage;
    if (string.IsNullOrEmpty(code))
        code = Script.GeneratedClassCodeWithLineNumbers;

    string result =
            "<style>" +
            "body { background: white; color; black; font-family: sans;}" +
            "</style>" +
            "<h1>Template Rendering Error</h3>\r\n<hr/>\r\n" +
            "<pre style='font-weight: 600;margin-bottom: 2em;'>" + WebUtility.HtmlEncode(errorMessage) + "</pre>\n\n" +
            "<pre>" + WebUtility.HtmlEncode(code) + "</pre>";                   

    return result;
}

Or you can present a nicer error page that itself is rendered through a template. This is the error page that is used in DM.

Display Error Messages
Figure 4 - Displaying render error information for debugging purposes

The code for this implementation is more complex.

public string ErrorHtml(RenderTemplateModel model, string errorMessage = null, string code = null)
  {
      if (string.IsNullOrEmpty(errorMessage))
          errorMessage = Script.ErrorMessage;
      if (string.IsNullOrEmpty(code))
          code = Script.GeneratedClassCodeWithLineNumbers;

      var origTemplateFile = model.Topic.GetRenderTemplatePath(model.Project.Settings.RenderTheme);            
      var templateFile = Path.Combine(Path.GetDirectoryName(origTemplateFile), "ErrorPage.html");
      string errorOutput = null;

      if (File.Exists(templateFile))
      {
      	  // render the error template
          var lastException = Script.ScriptEngine.LastException;
          model?.TemplateError = new TemplateError
          {
              Message = errorMessage,
              GeneratedCode = code,
              TemplateFile = origTemplateFile,
              CodeErrorMessage = errorMessage,
              CodeLineError = string.Empty,
              Exception = lastException
          };
          model.TemplateError.Message = model.TemplateError.WrapCompilerErrors(model.TemplateError.Message);
          model.TemplateError.ParseCompilerError();

          // Try to execute ErrorPage.html template

          bool generateScript = Script.SaveGeneratedClassCode;
          Script.SaveGeneratedClassCode = false;

          errorOutput = Script.ExecuteScriptFile(templateFile, model, basePath: model.Project.ProjectDirectory);

          Script.SaveGeneratedClassCode = generateScript;
      }

	  // if template doesn't exist or FAILs render a basic error page
      if (string.IsNullOrEmpty(errorOutput))
      {
          if (Script.ErrorMessage.Contains(" CS") && Script.ErrorMessage.Contains("):"))
          {
              errorOutput =
                      "<style>" +
                      "body { background: white; color: black; font-family: sans-serif; }" +
                      "</style>" +
                      "<h1>Template Compilation Error</h3>\r\n<hr/>\r\n" +
                      "<p style='margin-bottom: 2em; '>" + HtmlUtils.DisplayMemo(model.TemplateError.WrapCompilerErrors(errorMessage)) + "</pre>\n\n" +
                      (model.Topic.TopicState.TopicRenderMode == TopicRenderModes.Preview
                          ? "<pre>" + WebUtility.HtmlEncode(code) + "</pre>"
                          : "");
          }
          else
          {
              errorOutput =
                  "<style>" +
                  "body { background: white; color: black; font-family: sans-serif;}" +
                  "</style>" +
                  "<h1>Template Rendering Error</h3>\r\n<hr/>\r\n" +
                   "<p style='font-weight: 600;margin-bottom: 2em; '>" + WebUtility.HtmlEncode(errorMessage) + "</p>\n\n" +
                   (model.Topic.TopicState.TopicRenderMode == TopicRenderModes.Preview
                       ? "<hr/><pre>" + WebUtility.HtmlEncode(code) + "</pre>"
                       : "");
          }
      }

      return errorOutput;
  }

Summary

And that brings us to the end of this two-part post series. In this second part I've given a look behind the scenes of what's involved in running a scripting engine for site generation and local preview. As you can tell this is a little more involved than the basics of running a script template using the ScriptParser class as described in part one of this series.

Pathing is the main thing that can give you headaches - especially if there are multi-use cases of where your output is rendered. In the case of Documentation Monster, output can both be rendered into a final output Web site, or be used locally for previewing content from the file system and also be used for generating PDF and Print output which have additional specific requirements for pathing. The issues involved depend to some degrees on how you set up your application, but for me at least, I spent a lot of time getting the pathing to work correctly across all the output avenues. I hope that what I covered here will be of help in figuring out what needs to be considered, if not solving the issues directly.

I've been very happy with how using ScriptParser with its new features of Layout/Sections support works for this project. Given that I've been sitting in analysis paralysis for so long prior fighting with Razor, the results I've gotten are a huge relief, both in terms of features and functionality and performance. Hopefully the Westwind.Scripting library and the Script Templating will be useful to some of you as well.

Resources

© Rick Strahl, West Wind Technologies, 2005-2026
Posted in .NET  C#  
Read the whole story
alvinashcraft
4 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Copilot News Roundup

1 Share

Microsoft keeps moving fast, and April has brought so many Copilot updates that it is worth taking a look. We have a fresh capabilities in Word for high-stakes document workflows, a strong release wave across the Copilot apps, new security, and management, and analytics controls for admins .

Let’s take a closer look — and I will also walk through the upcoming features announced in Message Center.

  1. Copilot in Word — new capabilities for document workflows
  2. What’s new in Microsoft 365 Copilot — March 2026 highlights
  3. Latest enhancements for Copilot security, management, and analytics
  4. Important — Copilot Chat removed from Word, Excel, PowerPoint, and OneNote for non-Premium users
  5. Coming soon — from the Microsoft Admin Center (Message Center)
    1. MC1280557 — Submit agents to the Agent Store from Agent Builder
    2. MC1282682 — Agent Builder: a refreshed agent creation experience
    3. MC1222551 — Copilot Tuning: public preview and new Agent Builder templates
    4. MC1269241 — Anthropic models on by default for Copilot in Word, Excel, and PowerPoint
    5. MC1266023 — New Copilot metrics across Microsoft 365 apps
    6. MC1222978 — User-day export for Copilot Dashboard metrics (public preview)
    7. MC1280558 — Use Copilot to explain selected slide content in PowerPoint Live
  6. What this all means for the Future of Work

Copilot in Word — new capabilities for document workflows

If you work in legal, finance, or compliance — or in any team where document integrity is non-negotiable — this one is a meaningful leap. Microsoft is bringing a set of new Copilot capabilities to Word that preserve formatting, keep the collaboration history intact, and add real transparency to AI-assisted edits.

Here is what is new:

  • Track Changes natively in Word. Copilot’s edits are visible by default, and Track Changes can be enabled directly through Copilot — so every modification stays transparent, auditable, and granular.
  • Comment threads through Copilot. You can add, read, reply to, and manage comments via Copilot, and comments stay anchored to the relevant text.
  • Tables of contents done right. Copilot inserts and updates tables of contents using Word’s built-in heading styles, and the structure stays in sync as the document evolves.
  • Page elements and dynamic fields. Headers, footers, columns, margins — plus page numbers and dates — that Copilot can insert and manage, and that update automatically as the document changes.
  • Real-time progress for multi-step tasks. For longer jobs, Copilot now shows what it is working on as it works, so you can follow along with confidence.

These are rolling out on Windows desktop through the Frontier program on the Office Insiders Beta Channel, with Word for the web and Mac coming soon.

Read the announcement

What’s new in Microsoft 365 Copilot — March 2026 highlights

March was a busy month across every Copilot surface. A few of the items I think matter the most:

For users:

  • Video recap of meetings in Copilot Chat — a narrated highlight reel combining key takeaways with short clips, for meetings of at least 10 minutes with recording enabled. English first.
  • Researcher output formats — Researcher reports can now be converted to PowerPoint, PDF, infographic, or audio overview in one click.
  • Branded footer in the Microsoft 365 Copilot app — admins can configure a footer that builds trust that users are on an approved, organization-managed AI tool.
  • Audio recap in seven more languages — Chinese, French, German, Italian, Japanese, Portuguese, and Spanish.
  • AI in SharePoint (the refreshed Knowledge Agent) — agentic building and content intelligence inside SharePoint, powered by Anthropic’s Claude model. Public Preview in March, worldwide in May.
  • Copilot in Excel — Work IQ context automatically pulled in, plus multi-step edits to locally stored workbooks on Windows and Mac.
  • Citations display for Copilot in Word — citations automatically shown when responses use web or Work IQ sources.
  • Standardize format with Copilot in PowerPoint — fonts, sizes, and bullet styles cleaned up across all slides at once.

For admins:

  • Microsoft Purview DLP for Copilot safeguarding prompts and web searches containing sensitive data.
  • Authoritative sources, high-usage users, and domain exclusion — new controls in the Microsoft 365 admin center.
  • Satisfaction, intent, and usage tracking in the Copilot Dashboard.
  • Copilot Tuning templates in Agent Builder — for enterprises with at least 5,000 Copilot licenses, rolling to Frontier in April and worldwide in June. Read more from chapter below.

Read the full roundup

Latest enhancements for Copilot security, management, and analytics

As Copilot becomes a daily tool for more and more teams, IT and security leaders need practical, built-in controls — without slowing adoption. Microsoft published an excellent roundup of the newest ones:

  • Secure and Govern Microsoft 365 Copilot deployment guide — an updated foundational blueprint with three essential steps: remediate oversharing, implement reliable guardrails, and meet AI-related regulatory obligations. See aka.ms/Copilot/SecureGovern.
  • Microsoft Purview DLP for Copilot prompts — generally available.
  • Microsoft Purview DLP for web queries — Public Preview.
  • Purview DSPM: bulk remediation of overshared files — generally available.
  • Purview in the Microsoft 365 admin center — oversharing visibility and the ability to turn on Purview DLP for Copilot directly from there.
  • Organizational Messages — now includes email as a delivery channel, plus usage-based targeting for driving Copilot adoption based on real behavior. General availability this month.
  • Copilot Dashboard for everyone — now available to customers with at least 1 Microsoft 365 Copilot license, with metrics for users, trends, adoption, intensity, retention, and app-level breakdowns.
  • User satisfaction tracking at scale — thumbs-up and thumbs-down aggregated across apps, with trends over time and group comparisons.
  • New intent patterns across M365 apps — suggested reply, translate, coach, clean data, and more. Public Preview this month.
  • Export Copilot Dashboard data — de-identified CSV export, weekly metrics covering the past six months. GA this month.

Read the announcement

Important — Copilot Chat removed from Word, Excel, PowerPoint, and OneNote for non-Premium users

This one deserves its own section, because it has real impact on larger organizations.

Beginning April 15, 2026, Microsoft stopped offering direct access to Copilot Chat within Word, Excel, PowerPoint, and OneNote for users who do not have a paid Microsoft 365 Copilot (Premium) subscription. The Copilot icon is gone from the ribbon in those apps for unlicensed users. This change applies across all licenses, not just a single tenant — which is why it matters for every organization running a mixed Copilot license landscape.

What stays the same: Copilot Chat remains available on the web at m365.cloud.microsoft, in the Microsoft 365 Copilot app, inside Outlook (mail and calendar), and inside Teams — as long as users sign in with their work account to enable enterprise data protection.

Why this matters for larger organizations:

  • If your Copilot rollout was relying on the in-app Copilot Chat experience in Word / Excel / PowerPoint / OneNote for unlicensed users (for example, as a “try before you buy” surface), that surface is gone.
  • Users without a Microsoft 365 Copilot (Premium) license who were using the Copilot icon in those apps will notice the change immediately.
  • Communications and enablement materials should be updated so users know where Copilot Chat still lives and how to sign in with enterprise data protection.
  • For tenants where Premium licensing is not yet available at all (a situation familiar to many education and large public-sector customers), the in-app experience is simply not coming back without licensing.

The direction is clear: the rich in-app Copilot experience is reserved for Microsoft 365 Copilot (Premium) subscriptions, while Copilot Chat with enterprise data protection remains broadly available across the other surfaces.

Coming soon — from the Microsoft Admin Center (Message Center)

Several upcoming changes are already announced in the Microsoft Admin Center. Admins — these are the ones to put on your radar.

MC1280557 — Submit agents to the Agent Store from Agent Builder

Rolling out in mid-May 2026 and expected to complete by late May 2026. Microsoft 365 Roadmap ID 557173.

This one is a meaningful step for anyone scaling agent building inside the organization. Agent Builder users will be able to submit their agents for administrator review before the agents are published to the org’s Agent Store catalog. Once approved, the agent appears in the “Built by your org” section of the Agent Store, where colleagues can discover and install it.

How it works:

  • Submission — Agent Builder users select Submit to your org catalog to request publishing.
  • Review — the submission creates a review request in the Microsoft 365 admin center. Admins use the existing publishing and approval workflow, with full control over who can access the agent, scoping to specific users or security groups, and options for preinstalling or pinning.
  • Distribution — approved agents are available for everyone to discover and install directly from the Agent Store.
  • Updates — agent updates require a new admin submission, and each one triggers a new review cycle.

Published agents appear as a separate entry in the Agent Registry under Agents → All agents.

Action: no action is required before rollout, but admins can familiarize themselves with the review experience under Agents → All agents → Requests, and internal agent makers should be informed so they understand the submission and approval process.

MC1282682 — Agent Builder: a refreshed agent creation experience

Rolling out in late April 2026 (Worldwide, GCC, GCCH). Available to both Copilot Chat (Basic) and Microsoft 365 Copilot (Premium) users.

The Agent Builder creation experience is getting a visual and UX refresh to make building an agent clearer, faster, and more intuitive. The update focuses only on the creation experience — there are no changes to how agents function, how they are published, or how they are managed.

What is new:

  • The landing page now shows templates and a list of existing agents, making it easier to start fresh or reuse an agent.
  • Describe and Configure panes are displayed side by side — updates in Describe are reflected in the configuration in real time, with the configuration pane highlighting what changed and why.
  • A Show changes option lets users review how instructions have evolved over time.
  • Testing is now accessed through a Try it toggle, making it easy to switch between configuring and testing before publishing.

The feature is enabled by default and requires no admin configuration. A small update, but one that makes a daily builder experience noticeably smoother.

MC1222551 — Copilot Tuning: public preview and new Agent Builder templates

Public preview starts in April 2026 for organizations enrolled in the Frontier TAP and Public program with 5,000 or more Microsoft 365 Copilot licenses.

Copilot Tuning is adding new templates in Agent Builder designed for high-value document work — drafting complex documents, validating documents against organizational guidelines, and editing to match a distinct writing style. And with Copilot Tuning enabled, organizations can go further and tune those template-based agents by adjusting context, tools, and the underlying models with their own proprietary data, processes, and standards. This is how you get agents that genuinely work the way your organization works.

A few important details for admins:

  • The new templates are available to all users licensed for Microsoft 365 Copilot, even without Copilot Tuning itself.
  • Tuning is reserved for organizations with 5,000+ Copilot licenses in the Frontier TAP and Public program during preview.
  • Copilot Tuning creates a snapshot of selected SharePoint content for tuning purposes, stored in a tenant-isolated Microsoft 365 environment and used only within the Copilot Tuning service.
  • DLP policies on the source SharePoint content do not apply to the snapshot — this is an important nuance to review with your data governance team.
  • During public preview, EU tenant traffic remains within the EU Data Boundary, while global tenant traffic may be processed in other regions for large language model operations.
  • Advanced Data Residency (ADR) tenants: Copilot Tuning is not enabled by default; ADR customers may opt in by formally waiving ADR requirements through their Microsoft account team.
  • Snapshots are not automatically updated — if the underlying SharePoint content changes, agents must be retrained.

Action: admins can manage access in the Microsoft 365 admin center under Copilot → Settings → Copilot Tuning, scope tuning to specific users or Entra ID groups, or disable it entirely. Review internal data governance, privacy, and regulatory requirements before enabling — particularly around the DLP-on-snapshot nuance.

For a deeper dive, see the Copilot Tuning Overview on Microsoft Learn(opens in new window) or the Ignite session (45 min)(opens in new window).

MC1269241 — Anthropic models on by default for Copilot in Word, Excel, and PowerPoint

Starting May 4, 2026, a new “Copilot in M365 apps with Anthropic models” setting appears in the Microsoft 365 Admin Center. When enabled, Anthropic models become available by default for Copilot in Excel and PowerPoint, with Word support arriving in summer 2026.

A few important details:

  • This setting is enabled by default for your tenant (subject to Anthropic capacity). You can review or change it anytime under Copilot → Settings → View All → AI providers operating as Microsoft Subprocessor.
  • If your global Anthropic subprocessor setting is already enabled, this update introduces no additional change.
  • When Anthropic models are used, data processing for these models occurs outside the EU Data Boundary (EUDB). Anthropic operates as a Microsoft subprocessor under the Microsoft Product Terms and DPA. No customer data or state is stored outside the EUDB, and all data is encrypted in transit.
  • If no change is made, Anthropic models will be available for Copilot in Excel and PowerPoint from May 4, 2026. Word support joins in summer 2026.

Action: EU / EFTA / UK admins — review this setting now and confirm it aligns with your organization’s needs.

MC1266023 — New Copilot metrics across Microsoft 365 apps

Rolling out from late April 2026 and expected to complete in late May 2026. Microsoft 365 Roadmap ID 557981.

New metrics will appear in the Copilot Dashboard and Advanced Analysis in Copilot Analytics, giving admins better visibility into how users actually engage with Copilot:

  • Actions in the Microsoft 365 Copilot app, Microsoft Edge, and OneNote.
  • Intent-based scenarios in Outlook, Word, Excel, and PowerPoint — including suggested replies, translation, coaching, and clean data.

The feature is enabled automatically for all tenants with a Copilot license. No configuration or policy change is required.

MC1222978 — User-day export for Copilot Dashboard metrics (public preview)

Timeline: Public preview mid-May 2026 → end of May 2026, with GA early August 2026 → end of August 2026. Roadmap ID 547749.

Copilot Dashboard users with company-level access will be able to export de-identified Copilot usage metrics aggregated by user and day, covering the last 28 days. This is in addition to the existing user-week export.

Highlights:

  • Minimum of 50 Microsoft 365 Copilot licenses required.
  • Export includes Organization and Job function attributes, and supports analysis across Copilot-enabled apps (Word, Excel, Teams, and more).
  • Uses the same Viva Feature Access Management (VFAM) controls as the existing user-week export — no new access controls are introduced.
  • Data usually reflects Copilot activity up to 3 days before the export date.

Action: review your VFAM settings for users with company-wide Copilot Dashboard access, and inform eligible users about the new option.

MC1280558 — Use Copilot to explain selected slide content in PowerPoint Live

Rolling out in mid-May 2026 and expected to complete by late May 2026. Roadmap ID 557256.

During a Teams meeting with PowerPoint Live, attendees can select text on the slide — an acronym, a technical term, a complex concept — and ask Copilot to explain it. The explanation is shown privately to the attendee, without interrupting the presenter or cluttering the chat.

  • On by default for tenants with Microsoft 365 Copilot (Premium).
  • No admin action required to enable.
  • Processing follows existing Copilot for Microsoft 365 AI processing and security boundaries.

A small feature, but one with a big impact on meeting inclusivity — especially for global teams and for sessions full of jargon. I love this one.

What this all means for the work

Taken together, these updates tell a very clear story: Microsoft 365 Copilot is maturing from an assistant into a true coworker, with deeper app integration (Word document workflows, PowerPoint Live explanations), richer analytics (intent, satisfaction, user-day exports), and stronger security and governance (Purview DLP, oversharing remediation, Secure and Govern guidance). And with Copilot Cowork now in Frontier, the whole stack is moving toward real execution of multi-step work.

At the same time, the April 15 change reminds all of us that the rich in-app Copilot experience belongs to the Premium subscription (paid Microsoft 365 Copilot license), and every organization needs a clear licensing and enablement plan to match.

This is the Future of Work in practice — an AI-Native workplace where people and intelligent agents co-create every day, inside a secure, governed Microsoft 365. I am excited to see how organizations adopt all of this, and I will keep exploring these capabilities on the blog.

Stay tuned! ✨





Read the whole story
alvinashcraft
4 hours ago
reply
Pennsylvania, USA
Share this story
Delete

1.0.35

1 Share

2026-04-23

  • Slash commands support tab-completion for arguments and subcommands
  • Shell escape commands (!) now use your $SHELL when set, instead of always invoking /bin/sh
  • Permission prompts appear correctly in remote sessions for the CLI TUI
  • Session selector shows branch names, idle/in-use status, and has improved search with cursor support
  • Model change notification shows both the previous and new model name
  • /update and /version commands now honor your configured update channel
  • Session sync prompt uses clearer labels and explains GitHub.com cross-device sync
  • Support COPILOT_GH_HOST environment variable for GitHub hostname, taking precedence over GH_HOST
  • Press Ctrl+Y (in addition to Tab) to accept the highlighted option in completion popups (@-mentions, path completions, slash commands)
  • Add /session delete, delete , and delete-all subcommands, and x-to-delete in the session picker
  • MCP server names with spaces and special characters are now supported
  • Skill slash commands (e.g. /skill-name) passed as the initial prompt via -i are recognized correctly on startup
  • Shell completion notifications are not duplicated when read_bash already returned the result
  • --continue prefers resuming sessions from the current working directory instead of the most recently touched session
  • Status line script now includes context window fields that match the model badge and /context output
  • User settings are now stored in ~/.copilot/settings.json, separate from internal state in config.json
  • Name sessions with --name and resume them by name with --resume=
  • Configure Copilot agent now has shell access on Windows
  • Show a helpful error message with install instructions when clipboard utilities (wl-clipboard or xclip) are missing on Linux
  • LSP server entries in lsp.json support configurable spawn, initialization, and warmup timeouts
  • Context window indicator in the statusline is now hidden by default
  • Move MCP OAuth into the shared runtime flow and clear associated OAuth state when removing an MCP server.
  • Added a GitHub-style contribution graph to /usage that adapts to terminal color mode and falls back to distinct glyphs in no-color terminals
  • Self-correcting custom tool calls in agentic loop
  • Cursor movement, deletion, and rendering work correctly for emoji and multi-codepoint characters in the text input
  • Tool availability detection works correctly on Windows
  • Session token expiry during a turn is handled automatically without requiring you to resend your message
  • Initial tab and arrow key navigation in /cwd and /add-dir path picker selects the correct item
  • Transient I/O errors no longer appear as red error entries in the timeline when an IDE or extension disconnects
  • Custom agents and skills in ~/.claude/ are no longer incorrectly loaded as Copilot project config
  • Login command restores interactive input correctly after authentication
  • Improve rendering performance when displaying large amounts of text in the timeline
  • Sync task calls block until completion under MULTI_TURN_AGENTS instead of auto-promoting to background after 60s; sync no longer returns a reusable agent_id, use mode: "background" for follow-ups
  • Tab navigation supports Home/End keys to jump to first and last tab
  • Plugins take effect immediately after install without requiring a restart
  • Add continueOnAutoMode config option to automatically switch to auto model on rate limit instead of pausing
  • Auto mode no longer fails with an error when switching to a model that doesn't support the configured reasoning effort
  • Pattern-specific instruction files (.github/instructions/*.instructions.md) no longer include their full body in the system prompt on every session
  • Extension shutdown errors no longer appear as error-level log noise on every session exit
  • LSP refactoring tools now register correctly on the first turn when LSP configs are present
  • Add HTTP hook support, allowing hooks to POST JSON payloads to a configured URL instead of running a local command
  • Hide subagent thinking from the timeline
  • Custom agent name is now visible in the statusline footer and can be toggled via /statusline
  • Pressing Escape on startup dialogs no longer causes race conditions
  • grep and glob tools now accept multiple search paths
Read the whole story
alvinashcraft
4 hours ago
reply
Pennsylvania, USA
Share this story
Delete

An update on recent Claude Code quality reports

1 Share

An update on recent Claude Code quality reports

It turns out the high volume of complaints that Claude Code was providing worse quality results over the past two months was grounded in real problems.

The models themselves were not to blame, but three separate issues in the Claude Code harness caused complex but material problems which directly affected users.

Anthropic's postmortem describes these in detail. This one in particular stood out to me:

On March 26, we shipped a change to clear Claude's older thinking from sessions that had been idle for over an hour, to reduce latency when users resumed those sessions. A bug caused this to keep happening every turn for the rest of the session instead of just once, which made Claude seem forgetful and repetitive.

I frequently have Claude Code sessions which I leave for an hour (or often a day or longer) before returning to them. Right now I have 11 of those (according to ps aux  | grep 'claude ') and that's after closing down dozens more the other day.

I estimate I spend more time prompting in these "stale" sessions than sessions that I've recently started!

If you're building agentic systems it's worth reading this article in detail - the kinds of bugs that affect harnesses are deeply complicated, even if you put aside the inherent non-deterministic nature of the models themselves.

Via Hacker News

Tags: ai, prompt-engineering, generative-ai, llms, anthropic, coding-agents, claude-code

Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Grok Voice Think Fast 1.0

1 Share
Our most capable voice agent is now available via API.
Read the whole story
alvinashcraft
5 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Meta is laying off 10 percent of its staff

1 Share
Mark Zuckerberg presenting at Meta Connect on September 17th, 2025. | Bloomberg via Getty Images

Meta is planning to layoff around 10 percent of employees in May, according to a memo from the company's chief people officer, Janelle Gale, published by Bloomberg. That means approximately 8,000 people will see their jobs cut. Meta will also be closing around 6,000 open roles, according to Gale.

The cuts follow Meta's significant investments in AI, including spending huge sums to hire top talent and build data centers. The company forecast in January that it will spend $115 billion to $135 billion in capital expenditures in 2026 - a significant increase from its $72.22 billion in capital expenditures for 2025. The increase is to "support o …

Read the full story at The Verge.

Read the whole story
alvinashcraft
7 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories